- Add Dockerfile (multi-stage Node 20), .dockerignore, docker-compose.yml, and DEPLOY-DOKPLOY.md for container deployment on Dokploy. - Commit the DigitalOcean managed-Postgres Project CA cert (certs/ca-certificate.crt) so production TLS verification (fail-closed) works in-container. Public CA, safe to commit. - Blog cover images served from DO Spaces; allow *.digitaloceanspaces.com in the prod CSP img-src. - Includes the AI (case summaries) and Cloudflare Turnstile bot-protection features. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.5 KiB
Deploying eLegal Software on Dokploy
This app was previously deployed on Plesk + Passenger. It now ships as a single Docker container
(built from the repo Dockerfile) that serves the built React SPA and the Fastify API on one
port. Documents live in DigitalOcean Spaces and logs go to stdout, so the container is stateless
— no volumes needed.
- Runtime: Node 20, one process, listens on
0.0.0.0:$PORT(default8080). - Health check:
GET /api/health→200 {"ok":true}(no DB dependency). A deeperGET /api/health/dbverifies the database. - Database: external DigitalOcean Managed Postgres (unchanged).
- Storage: external DigitalOcean Spaces (unchanged).
1. Create the application in Dokploy
- Project → Create Application.
- Source → Git. Point it at this repo (
https://tea.serfaty.co/admin/elegalsoftware), branchmaster. Add a deploy key / token in Dokploy if the repo is private. - Build Type →
Dockerfile. Path:./Dockerfile(repo root). (Alternatively use the "Compose" type with the bundleddocker-compose.yml, but Application + Dockerfile is simpler — Dokploy wires Traefik and env for you.) - Port →
8080. This is the container port Dokploy/Traefik routes to.
2. Domain + TLS
- Domains → Add your hostname (e.g.
app.elegalsoftware.com), container port8080, and enable HTTPS / Let's Encrypt. Traefik terminates TLS and proxies to the container. - Point the hostname's DNS at the Dokploy server first so the ACME challenge can succeed.
3. Runtime environment variables
Set these in Environment (Dokploy injects them at container start). Use .env.example as the
authoritative list. The essentials:
| Variable | Notes |
|---|---|
NODE_ENV |
production |
PORT |
8080 (matches the exposed port) |
PUBLIC_URL |
Your public HTTPS URL, e.g. https://app.elegalsoftware.com. Used in emails and absolute links — must be the real domain in prod. |
COOKIE_DOMAIN |
Your apex/app domain (e.g. elegalsoftware.com). Leave blank only in local dev. |
SESSION_SECRET / CSRF_SECRET |
32+ byte hex each. Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))". |
SUPERADMIN_EMAILS |
Comma-separated. Only verified accounts on this list become superadmin. |
DATABASE_URL |
DO Postgres URL (...?sslmode=require). |
DATABASE_CA_CERT_PATH |
Path to the DO CA cert inside the container — required in prod (see §4). |
SPACES_ENDPOINT / SPACES_REGION / SPACES_BUCKET / SPACES_KEY / SPACES_SECRET |
Object storage (required). |
SMTP2GO_API_KEY / EMAIL_FROM |
Transactional email. |
ANTHROPIC_API_KEY |
AI features (optional; blank disables them). |
TURNSTILE_SECRET_KEY |
Bot-protection server key (see §5 for the site key). |
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET / STRIPE_PRICE_PRO / STRIPE_PRICE_LIFETIME |
Billing. |
SENTRY_DSN_API |
Optional error reporting. |
The API fails fast on boot if a required variable is missing (secrets,
SPACES_*, database). That's intentional — a misconfigured deploy stops loudly instead of running half-broken.
4. Database TLS — the one required extra step
In production the app refuses to connect over unverified TLS (no silent MITM exposure). You must give it the DigitalOcean CA certificate:
- In the DO control panel → your Postgres cluster → Download CA certificate.
- Make it available in the container, either:
- Commit it as
certs/ca-certificate.crt(a CA cert is public, safe to commit). The Dockerfile bakescerts/into the image. SetDATABASE_CA_CERT_PATH=./certs/ca-certificate.crt. - Or mount it via a Dokploy Volume/Mount (e.g. at
/app/certs/ca-certificate.crt) and setDATABASE_CA_CERT_PATHto that path.
- Commit it as
Without this, the container boots and /api/health still passes, but any request that touches the
database will error. (/api/health/db will return 503 until the cert is in place.)
5. Build-time variables (Vite) — easy to miss
VITE_* values are compiled into the browser bundle during vite build, so they must be set as
Build-time variables, not runtime env:
VITE_TURNSTILE_SITE_KEY— the public Turnstile site key.VITE_SENTRY_DSN— browser Sentry DSN (optional).
In Dokploy: Build → Build-time variables (passed as Docker build args). If you only set them as runtime env, the browser bundle won't pick them up and Turnstile won't render.
6. Database migrations
Run migrations as a deploy step, not automatically on every container start. In Dokploy, use a Run Command (or the app's terminal) after a deploy:
npm run db:migrate
Migrations are tracked (drizzle only applies pending ones) and safe to re-run. Keep the app at 1 replica while migrating; the invoice-numbering advisory locks handle write concurrency, but schema migrations should not run from multiple instances at once.
7. Stripe webhook
Add the endpoint in the Stripe dashboard:
https://<your-domain>/api/webhooks/stripe
The route reads the raw request body for signature verification. Unlike Plesk (which needed
proxy_request_buffering off), Traefik forwards the body fine — no extra proxy config required.
Set STRIPE_WEBHOOK_SECRET to the signing secret Stripe shows for that endpoint.
8. Deploy
Trigger a deploy in Dokploy (or enable auto-deploy on push). First deploy checklist:
- Runtime env vars set (§3), including
PUBLIC_URLandCOOKIE_DOMAINon the real domain. - DB CA cert in place and
DATABASE_CA_CERT_PATHset (§4). VITE_*set as build-time variables (§5).- Domain + Let's Encrypt configured (§2).
- Migrations run (§6).
- Stripe webhook pointed at the new URL (§7).
Verify: https://<domain>/api/health → {"ok":true}, then load the app, sign up, and open a
deep-link/refresh (e.g. /app) to confirm SPA routing.
Notes
- Local Docker test:
docker build -t elegal . && docker run --rm -p 8080:8080 --env-file .env elegalthen hithttp://localhost:8080/api/health. (Uses your local.env; the image itself never contains it —.envis in.dockerignore.) - Scaling: the app is stateless (Spaces storage, stdout logs, Postgres sessions), so it scales horizontally. Just run migrations as a single controlled step, not per-instance.
- Legacy Plesk files —
app.js,server.cjs,scripts/plesk-deploy.sh, andtmp/restart.txtare no longer used and can be deleted once the Dokploy cutover is confirmed.