A published database port puts the data on the public internet, and Docker's own iptables rules bypass ufw — the server looks firewalled while the port is open. Previously this was only a comment in compose.yaml, which an agent adding a db service later is free to ignore. Anchored at every point where a service could be added or shipped: - rules/security.md: the rule, the reasoning, and the audit commands - rules/backend.md + architecture SKILL: constraint when picking the data layer - agents/backend-dev.md: non-negotiable for any service it adds - agents/qa-engineer.md: a published port is a Critical finding - skills/deploy: blocking pre-flight audit - compose.yaml: db example now uses expose:, with depends_on healthcheck - docs/DEPLOYMENT.md + README: SSH tunnel instead of publishing for debugging
4.3 KiB
name, description, argument-hint, user-invocable
| name | description | argument-hint | user-invocable |
|---|---|---|---|
| deploy | Deploy the Docker Compose stack to a server, with pre-flight checks and rollback. | PROJ-X or 'production' | 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
- Read
features/INDEX.md— what is being shipped - 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:
npm run lint
npm run typecheck
npm test
npm run build
Then check:
- Every env var used in code is in
.env.examplewith 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:
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.
git clone <repo-url> /srv/<app-name> && cd /srv/<app-name>
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 toAPP_BIND— leave at127.0.0.1so the container is not exposed directlyBETTER_AUTH_SECRET—openssl rand -base64 32, unique per environment
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:
./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 <name> 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/healthreturns{"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:
git reset --hard <previous-sha>
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