Security was one section among seven in the QA Engineer. That is structurally weak: the agent that wants to recommend "production ready: YES" also judged security, and as a subsection it competes for attention with acceptance criteria, regression and test authoring. The new agent: - has no Write/Edit tools, so "never fix what you find" is enforced by the tool list rather than by instruction — it cannot launder a finding into a patch - reports PASS / FAIL / UNVERIFIED, and UNVERIFIED must reach the spec; an audit that omits what it could not check reads as all-clear - audits against .claude/rules/security.md, the same standard the developer agents are held to, so every mandatory item gets a verdict - holds a veto: BLOCK stops /deploy regardless of functional test results /qa now runs both agents in parallel and keeps the verdicts separate. The QA Engineer is explicitly forbidden from speaking for security.
4.7 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.
Security verdict is BLOCK, or missing entirely: → stop. A feature without a
Security Auditor verdict has not been security-reviewed, no matter how green the
functional tests are.
UNVERIFIED security items: → list them and ask the user explicitly whether to
ship with that surface unchecked. Do not decide this silently on their behalf.
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