# 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: - **`frontend`** — externes, von Traefik verwaltetes Netz (`external: true`, Name aus `TRAEFIK_NETWORK`, Default `frontend` — wie im feuerwehr_dashboard). Nur `app` hängt daran (Proxy↔App). - **`internal`** — internes Bridge-Netz; Postgres und die Geo-Dienste haben **keine veröffentlichten Ports** (nicht öffentlich erreichbar). Über dieses Netz hat der App-Container zugleich **Egress** (z. B. für den Authentik-OIDC-Token-Austausch). ## Voraussetzungen Das externe Traefik-Netz muss existieren, bevor der Stack startet: ```bash docker network create frontend ``` Die externe Traefik-Instanz muss: - einen Entrypoint `websecure` (Port 443) bereitstellen, - einen Zertifikatsauflöser anbieten, dessen Name `TRAEFIK_CERTRESOLVER` entspricht (Default `letsencrypt`), - am Netz `frontend` 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. `florian.feuerwehr-rems.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 | | `AUTHENTIK_ADMIN_GROUP` | Authentik-Gruppe → platform_admin (Default `floriannetz-admins`; s. authentik-setup.md) | | `DATABASE_URL` | wird in Compose aus `POSTGRES_*` zusammengesetzt | | `TRAEFIK_CERTRESOLVER` | Name des Traefik-Zertifikatsauflösers | | `TRAEFIK_NETWORK` | Name des externen Traefik-Netzes (Default `frontend`) | ## 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`. **Admin-Zugang über Gruppe:** Dem Provider muss das `groups`-Scope-Mapping zugewiesen sein, und es muss die Gruppe aus `AUTHENTIK_ADMIN_GROUP` existieren — nur deren Mitglieder werden `platform_admin`. Details: `authentik-setup.md`. ## 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 frontend # 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://`.