--- name: deploy description: Deploy the Docker Compose stack to a server, with pre-flight checks and rollback. argument-hint: "PROJ-X or 'production'" user-invocable: true --- # Deploy Self-hosted Docker Compose. There is no Vercel, no managed platform. The stack runs one app container; TLS and routing are handled by a reverse proxy already present on the host. ## 1. Gate 1. Read `features/INDEX.md` — what is being shipped 2. Read the feature spec's QA section **No QA section:** → "Für PROJ-X gibt es noch kein QA-Ergebnis. Führe erst `/qa` aus." → Stop. **Critical or High bugs open:** → name them and stop. Those get fixed first. ## 2. Pre-flight Run and show the results — do not proceed on a failure: ```bash npm run lint npm run typecheck npm test npm run build ``` Then check: - [ ] Every env var used in code is in `.env.example` with a dummy value - [ ] No secrets in the diff (`git diff --cached`) - [ ] Pending migrations are committed - [ ] Everything committed and pushed to the remote the server pulls from **Port audit — blocking.** Only the app service may publish a port, and only on loopback: ```bash docker compose config | grep -B8 "published:" ``` Any service other than `app` with a published port, or any binding to `0.0.0.0`, stops the deploy. A published database port puts the data on the public internet and bypasses ufw, because Docker writes its own iptables rules. Fix `compose.yaml` before continuing. ## 3. First deployment only Walk the user through this once. It happens on the server, over SSH. **Prerequisites the user provides manually:** a server with Docker and the Compose plugin, a reverse proxy (nginx, Caddy, Traefik) terminating TLS, and a DNS record pointing at it. ```bash git clone /srv/ && cd /srv/ cp .env.example .env ``` Then edit `.env`: - `NEXT_PUBLIC_APP_URL` — the real public URL. **If auth is used, this must match the domain exactly**: the WebAuthn relying-party ID comes from this hostname, and a mismatch makes every passkey fail without a useful error. - `APP_PORT` — the port the reverse proxy forwards to - `APP_BIND` — leave at `127.0.0.1` so the container is not exposed directly - `BETTER_AUTH_SECRET` — `openssl rand -base64 32`, unique per environment ```bash docker compose up -d --build curl -fsS http://127.0.0.1:${APP_PORT:-3000}/api/health ``` Point the reverse proxy at `127.0.0.1:$APP_PORT` and make sure it forwards `X-Forwarded-Proto` and `X-Forwarded-Host`, otherwise redirects and WebAuthn origin checks break. Passkeys need HTTPS in production — confirm TLS works before testing auth. ## 4. Subsequent deployments On the server: ```bash ./scripts/update.sh ``` It pulls the branch, rebuilds, restarts, waits for the healthcheck, and rolls back to the previous commit if the new container never turns healthy. `--force` rebuilds without a new commit; `--branch ` deploys a different branch. If a data layer exists, migrations run before the new container serves traffic — see `docs/DEPLOYMENT.md`. ## 5. Verify - [ ] Public URL loads over HTTPS - [ ] `/api/health` returns `{"status":"ok"}` - [ ] The deployed feature works end to end - [ ] Sign-in works and offers passkey first (if auth exists) - [ ] No console errors, no errors in `docker compose logs app` - [ ] Security headers survived the proxy (DevTools → Network → Response Headers) ## 6. Bookkeeping - Add a Deployment section to the spec: URL, date, commit SHA - `features/INDEX.md` → **Deployed** (write, then re-read to verify) - Tag: `git tag -a v1.X.0-PROJ-X -m "Deploy PROJ-X: [name]"` and push it ## Rollback `scripts/update.sh` rolls back automatically on an unhealthy container. Manually: ```bash git reset --hard docker compose up -d --build ``` ## Common failures | Symptom | Cause | |---------|-------| | Build works locally, fails in Docker | A dependency sits in devDependencies but is needed at build time | | Env var undefined at runtime | Not in `.env` on the server, or missing `NEXT_PUBLIC_` for client use | | Container healthy, proxy returns 502 | Proxy points at the wrong port, or `APP_BIND` is not reachable from it | | Passkeys fail after deploy | `NEXT_PUBLIC_APP_URL` does not match the real domain, or TLS is missing | | Redirect loop | Proxy not forwarding `X-Forwarded-Proto` | ## Commit ``` deploy(PROJ-X): Deploy [feature name] to production ```