Template
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
137 lines
4.3 KiB
Markdown
137 lines
4.3 KiB
Markdown
---
|
|
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 <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 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 <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/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 <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
|
|
```
|