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>
133 lines
6.5 KiB
Markdown
133 lines
6.5 KiB
Markdown
# 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.
|