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>
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 ausTRAEFIK_NETWORK, Defaulttraefik). Nurapphä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_CERTRESOLVERentspricht (Defaultletsencrypt), - am Netz
traefiklauschen (providers.dockermitexposedByDefault=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 wertetX-Forwarded-Proto/X-Forwarded-Hostaus.AUTH_URL=https://${APP_HOST}— erzwingt diehttps://-Origin für Callback-URLs und aktiviert das__Secure--Cookie-Präfix (Cookiesecure,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:
- wartet via
pg_isreadyauf Postgres, - wendet die Drizzle-Migrationen idempotent an (
node docker/migrate.mjs), - optional (
RUN_SEED=true) den NÖ-Katalog-Seed, - 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→ genauapp 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/302mit gültigem TLS-Zertifikat.- Login über Authentik setzt ein
__Secure--Cookie; Callback-URL isthttps://.