Dokploy deploy: Dockerfile, DB CA cert, blog images, CSP
CI / build-and-test (push) Has been cancelled

- 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>
This commit is contained in:
Leon Serfaty
2026-07-17 13:04:18 -04:00
co-authored by Claude Fable 5
parent d9b807662a
commit d1d96e4dd2
29 changed files with 1224 additions and 28 deletions
+132
View File
@@ -0,0 +1,132 @@
# 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/health``200 {"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:
```bash
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 files** — `app.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.