# 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://`.
