Workstream 10: Deployment (Docker + externes Traefik) (Phase 7)
Liefert das reproduzierbare Compose-Setup hinter EXTERNEM Traefik: - Dockerfile (multi-stage deps/builder/runner, Next.js standalone, non-root UID/GID 1001, HEALTHCHECK gegen /api/health). - docker/entrypoint.sh: wartet via pg_isready auf Postgres, wendet Migrationen idempotent an (docker/migrate.mjs, plain ESM ohne tsx/drizzle-kit), optionaler Seed (RUN_SEED), dann exec node server.js. - docker-compose.yml: genau vier Services (app, postgres, osrm, nominatim), KEIN Proxy-Service; externes traefik-Netz + internes Netz; Traefik-Labels (Host, websecure, tls.certresolver, Security-Header-Middleware); Postgres-/App-Healthchecks; AUTH_URL/AUTH_TRUST_HOST/Forwarded-Header. - docker-compose.override.yml.example: lokal :3000 ohne TLS (http AUTH_URL). - .dockerignore, Makefile (build/up/down/logs/deploy/data/config). - .env.example: voller Vertrag inkl. APP_HOST, TRAEFIK_*, POSTGRES_*, RUN_SEED. - docs/reference/deployment-traefik.md: externes Netz, Authentik-Redirect-URI https://${APP_HOST}/api/auth/callback/authentik, Forwarded-Header/Cookies, /api/health-Allowlist. - tests/unit/deployment.test.ts (TDD): statische Offline-Verifikation der Artefakte; vitest.config.ts nimmt tests/unit/** auf. Offline verifiziert: tsc --noEmit sauber; vitest run grün (200 passed, 7 db-roundtrip skipped); next build erzeugt .next/standalone/server.js; sh -n docker/entrypoint.sh ok; make -n deploy zeigt build->up. Deferred (kein Docker/Postgres in der Sandbox): docker build/run id -u=1001, docker compose config --services, /api/health anonym 200, End-to-End Traefik. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
130
docs/reference/deployment-traefik.md
Normal file
130
docs/reference/deployment-traefik.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Deployment hinter externem Traefik
|
||||
|
||||
FlorianNetz wird als Docker-Compose-Stack betrieben und **hinter einer separat
|
||||
betriebenen Traefik-Instanz** ausgeliefert. Es gibt bewusst **keinen** eigenen
|
||||
Proxy-/Traefik-Service im Compose-Stack — Routing und TLS-Terminierung übernimmt
|
||||
das externe Traefik.
|
||||
|
||||
## Komponenten
|
||||
|
||||
Der Stack besteht aus genau vier Services (kein Proxy):
|
||||
|
||||
| Service | Zweck |
|
||||
| ----------- | -------------------------------------------------- |
|
||||
| `app` | Next.js-Standalone-Server (non-root, UID 1001) |
|
||||
| `postgres` | PostgreSQL 16 (Daten-Volume, `pg_isready`-Health) |
|
||||
| `osrm` | OSRM-Routing (Österreich-Extrakt, `/table`) |
|
||||
| `nominatim` | Geocoding (Österreich-Extrakt, `/search`) |
|
||||
|
||||
Netze:
|
||||
|
||||
- **`traefik`** — externes, von Traefik verwaltetes Netz (`external: true`,
|
||||
Name aus `TRAEFIK_NETWORK`, Default `traefik`). Nur `app` hängt daran.
|
||||
- **`internal`** — internes Netz (`internal: true`); Postgres und die Geo-Dienste
|
||||
sind ausschließlich für die App erreichbar, nie öffentlich.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Das externe Traefik-Netz muss existieren, bevor der Stack startet:
|
||||
|
||||
```bash
|
||||
docker network create traefik
|
||||
```
|
||||
|
||||
Die externe Traefik-Instanz muss:
|
||||
|
||||
- einen Entrypoint `websecure` (Port 443) bereitstellen,
|
||||
- einen Zertifikatsauflöser anbieten, dessen Name `TRAEFIK_CERTRESOLVER`
|
||||
entspricht (Default `letsencrypt`),
|
||||
- am Netz `traefik` lauschen (`providers.docker` mit `exposedByDefault=false`).
|
||||
|
||||
Die App-Labels in `docker-compose.yml` setzen Router (`Host(\`${APP_HOST}\`)`,
|
||||
`entrypoints=websecure`, `tls.certresolver`), Service-Port `3000` und eine
|
||||
Security-Header-Middleware (defense-in-depth zusätzlich zu `next.config.ts`).
|
||||
|
||||
## Pflicht-Umgebungsvariablen
|
||||
|
||||
Vollständiger Vertrag in `.env.example`. Für den Betrieb hinter Traefik zwingend:
|
||||
|
||||
| Variable | Beispiel / Hinweis |
|
||||
| ------------------------- | ---------------------------------------------------- |
|
||||
| `APP_HOST` | öffentlicher Hostname, z. B. `floriannetz.example.at` |
|
||||
| `AUTH_URL` | `https://${APP_HOST}` — Basis für Callback + Cookies |
|
||||
| `AUTH_TRUST_HOST` | `true` — Auth.js vertraut den Forwarded-Headern |
|
||||
| `AUTH_SECRET` | >= 32 Zeichen (`openssl rand -base64 32`) |
|
||||
| `AUTHENTIK_ISSUER` | OIDC-Issuer-URL der Authentik-Anwendung |
|
||||
| `AUTHENTIK_CLIENT_ID` | Client-ID der Authentik-Anwendung |
|
||||
| `AUTHENTIK_CLIENT_SECRET` | Client-Secret der Authentik-Anwendung |
|
||||
| `DATABASE_URL` | wird in Compose aus `POSTGRES_*` zusammengesetzt |
|
||||
| `TRAEFIK_CERTRESOLVER` | Name des Traefik-Zertifikatsauflösers |
|
||||
| `TRAEFIK_NETWORK` | Name des externen Traefik-Netzes (Default `traefik`) |
|
||||
|
||||
## Forwarded-Header & sichere Cookies
|
||||
|
||||
Hinter Traefik terminiert TLS am Proxy; die App sieht intern HTTP. Damit
|
||||
Auth.js die korrekte Origin erkennt und sichere Cookies setzt:
|
||||
|
||||
- `AUTH_TRUST_HOST=true` — Auth.js wertet `X-Forwarded-Proto`/`X-Forwarded-Host`
|
||||
aus.
|
||||
- `AUTH_URL=https://${APP_HOST}` — erzwingt die `https://`-Origin für
|
||||
Callback-URLs und aktiviert das `__Secure-`-Cookie-Präfix (Cookie `secure`,
|
||||
`httpOnly`, `sameSite=lax`).
|
||||
|
||||
Bei lokaler HTTP-Entwicklung (`docker-compose.override.yml`) ist
|
||||
`AUTH_URL=http://localhost:3000` — dann fällt das `secure`/`__Secure-`-Verhalten
|
||||
weg, sonst bräche der Login über HTTP.
|
||||
|
||||
## Authentik-Konfiguration
|
||||
|
||||
In der Authentik-Anwendung (OAuth2/OpenID-Provider) als **Redirect-URI**
|
||||
eintragen:
|
||||
|
||||
```
|
||||
https://${APP_HOST}/api/auth/callback/authentik
|
||||
```
|
||||
|
||||
Der Pfad `callback/authentik` entspricht dem NextAuth-Provider-Namen. Bei lokaler
|
||||
Entwicklung zusätzlich `http://localhost:3000/api/auth/callback/authentik`.
|
||||
|
||||
## Health-Check & Middleware-Allowlist
|
||||
|
||||
`GET /api/health` ist **öffentlich** (anonym `200`, nur Liveness, keine
|
||||
Fachdaten). Die Edge-Middleware nimmt den Pfad in ihrer Allowlist aus
|
||||
(`api/health` im `matcher`), sonst würde die Default-deny-Schicht ihn auf
|
||||
`/login` umleiten. Sowohl der Container-`HEALTHCHECK` als auch der Compose-
|
||||
App-Healthcheck pingen `http://127.0.0.1:3000/api/health`.
|
||||
|
||||
## Migration & Seed beim Deploy
|
||||
|
||||
`docker/entrypoint.sh` läuft vor dem App-Start:
|
||||
|
||||
1. wartet via `pg_isready` auf Postgres,
|
||||
2. wendet die Drizzle-Migrationen idempotent an (`node docker/migrate.mjs`),
|
||||
3. optional (`RUN_SEED=true`) den NÖ-Katalog-Seed,
|
||||
4. startet `exec node server.js`.
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
cp .env.example .env # Werte setzen (APP_HOST, AUTH_*, AUTHENTIK_*, POSTGRES_*)
|
||||
docker network create traefik # einmalig, falls nicht vorhanden
|
||||
make data # einmalig: OSRM-Geodaten vorbereiten (groß, dauert)
|
||||
make deploy # build + up
|
||||
```
|
||||
|
||||
## Lokal ohne Traefik
|
||||
|
||||
```bash
|
||||
cp docker-compose.override.yml.example docker-compose.override.yml
|
||||
docker compose up -d
|
||||
# App: http://localhost:3000
|
||||
```
|
||||
|
||||
## Verifikation
|
||||
|
||||
- `docker compose -f docker-compose.yml -f docker-compose.geo.yml config --services`
|
||||
→ genau `app postgres osrm nominatim` (kein Proxy).
|
||||
- `docker build -t floriannetz . && docker run --rm floriannetz id -u` → `1001`.
|
||||
- `sh -n docker/entrypoint.sh` → keine Syntaxfehler.
|
||||
- `curl -I https://${APP_HOST}` → `200`/`302` mit gültigem TLS-Zertifikat.
|
||||
- Login über Authentik setzt ein `__Secure-`-Cookie; Callback-URL ist `https://`.
|
||||
Reference in New Issue
Block a user