# 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:///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:///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.