Files
Florian-netz/docs/reference/deployment-traefik.md
Matthias Hochmeister d50ec765ab 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>
2026-06-09 12:35:45 +02:00

5.3 KiB

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:

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 3000und eine Security-Header-Middleware (defense-in-depth zusätzlich zunext.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

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

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 -u1001.
  • 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://.