Files
elegalsoftware/DEPLOY-DOKPLOY.md
Leon SerfatyandClaude Fable 5 d1d96e4dd2
CI / build-and-test (push) Has been cancelled
Dokploy deploy: Dockerfile, DB CA cert, blog images, CSP
- 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>
2026-07-17 13:04:18 -04:00

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 (default 8080).
  • Health check: GET /api/health200 {"ok":true} (no DB dependency). A deeper GET /api/health/db verifies the database.
  • Database: external DigitalOcean Managed Postgres (unchanged).
  • Storage: external DigitalOcean Spaces (unchanged).

1. Create the application in Dokploy

  1. Project → Create Application.
  2. Source → Git. Point it at this repo (https://tea.serfaty.co/admin/elegalsoftware), branch master. Add a deploy key / token in Dokploy if the repo is private.
  3. Build Type → Dockerfile. Path: ./Dockerfile (repo root). (Alternatively use the "Compose" type with the bundled docker-compose.yml, but Application + Dockerfile is simpler — Dokploy wires Traefik and env for you.)
  4. Port → 8080. This is the container port Dokploy/Traefik routes to.

2. Domain + TLS

  • Domains → Add your hostname (e.g. app.elegalsoftware.com), container port 8080, 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:

  1. In the DO control panel → your Postgres cluster → Download CA certificate.
  2. Make it available in the container, either:
    • Commit it as certs/ca-certificate.crt (a CA cert is public, safe to commit). The Dockerfile bakes certs/ into the image. Set DATABASE_CA_CERT_PATH=./certs/ca-certificate.crt.
    • Or mount it via a Dokploy Volume/Mount (e.g. at /app/certs/ca-certificate.crt) and set DATABASE_CA_CERT_PATH to that path.

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_URL and COOKIE_DOMAIN on the real domain.
  • DB CA cert in place and DATABASE_CA_CERT_PATH set (§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 elegal then hit http://localhost:8080/api/health. (Uses your local .env; the image itself never contains it — .env is 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 filesapp.js, server.cjs, scripts/plesk-deploy.sh, and tmp/restart.txt are no longer used and can be deleted once the Dokploy cutover is confirmed.