deploy: Traefik-Setup an feuerwehr_dashboard angleichen

Abgeglichen mit ~/work/feuerwehr_dashboard/docker-compose.yml:
- externes Traefik-Netz heißt 'frontend' (external: true), nicht 'traefik'
- explizite Router->Service-Bindung (routers.floriannetz.service=floriannetz)
- entrypoints=websecure, tls + certresolver=letsencrypt, port 3000
- traefik.docker.network -> frontend; AUTHENTIK_ADMIN_GROUP an App durchgereicht
- internes Netz als Bridge (statt internal:true): Postgres/Geo ohne Host-Ports,
  aber App hat Egress für Authentik-OIDC
- APP_HOST-Default florian.feuerwehr-rems.at; TRAEFIK_NETWORK-Default frontend
- Doku (deployment-traefik.md) + Makefile-Kommentare angepasst

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-10 12:39:42 +02:00
parent f2578cedab
commit f71cf51eb4
4 changed files with 50 additions and 32 deletions

View File

@@ -31,11 +31,13 @@ HAVERSINE_KMH=50
# Deployment / externes Traefik # Deployment / externes Traefik
# APP_HOST ist der öffentliche Hostname (Traefik-Routing + AUTH_URL-Basis). # APP_HOST ist der öffentliche Hostname (Traefik-Routing + AUTH_URL-Basis).
# In Produktion: AUTH_URL=https://${APP_HOST} und AUTH_TRUST_HOST=true setzen. # In Produktion: AUTH_URL=https://${APP_HOST} und AUTH_TRUST_HOST=true setzen.
APP_HOST=floriannetz.example.at APP_HOST=florian.feuerwehr-rems.at
# Traefik-Zertifikatsauflöser (muss in der externen Traefik-Instanz definiert sein). # Traefik-Zertifikatsauflöser (muss in der externen Traefik-Instanz definiert sein).
TRAEFIK_CERTRESOLVER=letsencrypt TRAEFIK_CERTRESOLVER=letsencrypt
# Name des externen, von Traefik verwalteten Docker-Netzes. # Name des externen, von Traefik verwalteten Docker-Netzes
TRAEFIK_NETWORK=traefik # (im feuerwehr_dashboard heißt es "frontend"). Muss existieren:
# docker network create frontend
TRAEFIK_NETWORK=frontend
# Optionaler Katalog-Seed beim Container-Start (idempotent). # Optionaler Katalog-Seed beim Container-Start (idempotent).
RUN_SEED=false RUN_SEED=false
# Postgres-Zugangsdaten für den Compose-Postgres-Service. # Postgres-Zugangsdaten für den Compose-Postgres-Service.

View File

@@ -9,7 +9,7 @@
# make build-app migrate # make build-app migrate
# #
# Voll-Deploy hinter externem Traefik (Docker): # Voll-Deploy hinter externem Traefik (Docker):
# docker network create traefik # einmalig # docker network create frontend # einmalig (externes Traefik-Netz)
# make deploy # make deploy
# #
# `make help` listet alle Ziele. # `make help` listet alle Ziele.
@@ -116,7 +116,7 @@ setup: install env db-up db-wait migrate seed-all ## Komplettes lokales Setup vo
@echo "✓ Setup fertig. Login-Admin via 'make seed-auth' angelegt. Weiter mit: make dev" @echo "✓ Setup fertig. Login-Admin via 'make seed-auth' angelegt. Weiter mit: make dev"
# --- Deployment (externes Traefik; braucht Docker) ----------------------- # --- Deployment (externes Traefik; braucht Docker) -----------------------
# Externes Netz muss existieren: docker network create traefik # Externes Netz muss existieren: docker network create frontend
.PHONY: build up down logs ps deploy migrate-stack data config .PHONY: build up down logs ps deploy migrate-stack data config
build: ## App-Image bauen (Next.js standalone, non-root) build: ## App-Image bauen (Next.js standalone, non-root)
$(COMPOSE) build app $(COMPOSE) build app

View File

@@ -1,18 +1,22 @@
# FlorianNetz — Basis-Compose hinter EXTERNEM Traefik. # FlorianNetz — Basis-Compose hinter EXTERNEM Traefik.
# #
# Ausgerichtet auf das bestehende Setup von feuerwehr_dashboard:
# - externes, von Traefik verwaltetes Netz heißt "frontend" (external: true)
# - Router: entrypoints=websecure, tls + certresolver=letsencrypt
# - explizite Router->Service-Bindung, loadbalancer.server.port=3000
# - traefik.docker.network = das externe "frontend"-Netz
#
# Es gibt bewusst KEINEN eigenen Proxy-/Traefik-Service: Routing/TLS übernimmt # Es gibt bewusst KEINEN eigenen Proxy-/Traefik-Service: Routing/TLS übernimmt
# eine separat betriebene Traefik-Instanz, die am externen Netz "${TRAEFIK_NETWORK}" # die separat betriebene Traefik-Instanz am Netz "${TRAEFIK_NETWORK}" (Default:
# (Default: traefik) lauscht. Dieses Netz muss bereits existieren: # frontend). Dieses Netz muss bereits existieren:
# docker network create traefik # docker network create frontend
# #
# Geo-Dienste (osrm, nominatim) sind hier mit ihren Laufzeit-Verträgen definiert; # Postgres/Geo liegen am internen Bridge-Netz (keine veröffentlichten Ports,
# das schwergewichtige Daten-Preprocessing/Volume kommt aus docker-compose.geo.yml # also nicht öffentlich erreichbar) — der App-Container hat über dieses Netz
# (siehe scripts/prepare-osm-data.sh / infra/geo). # zugleich Egress (z. B. für den Authentik-OIDC-Token-Austausch).
# #
# Start: # Start: docker compose --env-file .env up -d
# docker compose --env-file .env up -d # Lokal: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# Lokal ohne Traefik/TLS:
# docker compose -f docker-compose.yml -f docker-compose.override.yml up -d
services: services:
app: app:
@@ -32,13 +36,14 @@ services:
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER} AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER}
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID} AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET} AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET}
AUTHENTIK_ADMIN_GROUP: ${AUTHENTIK_ADMIN_GROUP:-floriannetz-admins}
OSRM_URL: http://osrm:5000 OSRM_URL: http://osrm:5000
NOMINATIM_URL: http://nominatim:8080 NOMINATIM_URL: http://nominatim:8080
GEO_HTTP_TIMEOUT_MS: ${GEO_HTTP_TIMEOUT_MS:-4000} GEO_HTTP_TIMEOUT_MS: ${GEO_HTTP_TIMEOUT_MS:-4000}
HAVERSINE_KMH: ${HAVERSINE_KMH:-50} HAVERSINE_KMH: ${HAVERSINE_KMH:-50}
RUN_SEED: ${RUN_SEED:-false} RUN_SEED: ${RUN_SEED:-false}
networks: networks:
- traefik - frontend
- internal - internal
healthcheck: healthcheck:
test: test:
@@ -51,11 +56,12 @@ services:
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.docker.network=${TRAEFIK_NETWORK:-traefik}" - "traefik.docker.network=${TRAEFIK_NETWORK:-frontend}"
- "traefik.http.routers.floriannetz.rule=Host(`${APP_HOST}`)"
- "traefik.http.routers.floriannetz.entrypoints=websecure" - "traefik.http.routers.floriannetz.entrypoints=websecure"
- "traefik.http.routers.floriannetz.rule=Host(`${APP_HOST}`)"
- "traefik.http.routers.floriannetz.tls=true" - "traefik.http.routers.floriannetz.tls=true"
- "traefik.http.routers.floriannetz.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}" - "traefik.http.routers.floriannetz.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}"
- "traefik.http.routers.floriannetz.service=floriannetz"
- "traefik.http.services.floriannetz.loadbalancer.server.port=3000" - "traefik.http.services.floriannetz.loadbalancer.server.port=3000"
# Security-Header-Middleware (zusätzlich zu next.config.ts; defense-in-depth). # Security-Header-Middleware (zusätzlich zu next.config.ts; defense-in-depth).
- "traefik.http.routers.floriannetz.middlewares=floriannetz-sechdrs" - "traefik.http.routers.floriannetz.middlewares=floriannetz-sechdrs"
@@ -137,10 +143,12 @@ volumes:
nominatim-data: nominatim-data:
networks: networks:
# Externes, von der separaten Traefik-Instanz verwaltetes Netz. # Externes, von der separaten Traefik-Instanz verwaltetes Netz (wie im
traefik: # feuerwehr_dashboard "frontend"). Muss existieren: docker network create frontend
frontend:
external: true external: true
name: ${TRAEFIK_NETWORK:-traefik} name: ${TRAEFIK_NETWORK:-frontend}
# Internes Netz: Postgres/Geo sind nur app-intern erreichbar, nicht öffentlich. # Internes Bridge-Netz: Postgres/Geo ohne veröffentlichte Ports (nicht
# öffentlich), zugleich Egress für den App-Container (Authentik-OIDC).
internal: internal:
internal: true driver: bridge

View File

@@ -18,17 +18,20 @@ Der Stack besteht aus genau vier Services (kein Proxy):
Netze: Netze:
- **`traefik`** — externes, von Traefik verwaltetes Netz (`external: true`, - **`frontend`** — externes, von Traefik verwaltetes Netz (`external: true`,
Name aus `TRAEFIK_NETWORK`, Default `traefik`). Nur `app` hängt daran. Name aus `TRAEFIK_NETWORK`, Default `frontend` — wie im feuerwehr_dashboard).
- **`internal`** — internes Netz (`internal: true`); Postgres und die Geo-Dienste Nur `app` hängt daran (Proxy↔App).
sind ausschließlich für die App erreichbar, nie öffentlich. - **`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 ## Voraussetzungen
Das externe Traefik-Netz muss existieren, bevor der Stack startet: Das externe Traefik-Netz muss existieren, bevor der Stack startet:
```bash ```bash
docker network create traefik docker network create frontend
``` ```
Die externe Traefik-Instanz muss: Die externe Traefik-Instanz muss:
@@ -36,7 +39,7 @@ Die externe Traefik-Instanz muss:
- einen Entrypoint `websecure` (Port 443) bereitstellen, - einen Entrypoint `websecure` (Port 443) bereitstellen,
- einen Zertifikatsauflöser anbieten, dessen Name `TRAEFIK_CERTRESOLVER` - einen Zertifikatsauflöser anbieten, dessen Name `TRAEFIK_CERTRESOLVER`
entspricht (Default `letsencrypt`), entspricht (Default `letsencrypt`),
- am Netz `traefik` lauschen (`providers.docker` mit `exposedByDefault=false`). - am Netz `frontend` lauschen (`providers.docker` mit `exposedByDefault=false`).
Die App-Labels in `docker-compose.yml` setzen Router (`Host(\`${APP_HOST}\`)`, Die App-Labels in `docker-compose.yml` setzen Router (`Host(\`${APP_HOST}\`)`,
`entrypoints=websecure`, `tls.certresolver`), Service-Port `3000` und eine `entrypoints=websecure`, `tls.certresolver`), Service-Port `3000` und eine
@@ -48,16 +51,17 @@ Vollständiger Vertrag in `.env.example`. Für den Betrieb hinter Traefik zwinge
| Variable | Beispiel / Hinweis | | Variable | Beispiel / Hinweis |
| ------------------------- | ---------------------------------------------------- | | ------------------------- | ---------------------------------------------------- |
| `APP_HOST` | öffentlicher Hostname, z. B. `floriannetz.example.at` | | `APP_HOST` | öffentlicher Hostname, z. B. `florian.feuerwehr-rems.at` |
| `AUTH_URL` | `https://${APP_HOST}` — Basis für Callback + Cookies | | `AUTH_URL` | `https://${APP_HOST}` — Basis für Callback + Cookies |
| `AUTH_TRUST_HOST` | `true` — Auth.js vertraut den Forwarded-Headern | | `AUTH_TRUST_HOST` | `true` — Auth.js vertraut den Forwarded-Headern |
| `AUTH_SECRET` | >= 32 Zeichen (`openssl rand -base64 32`) | | `AUTH_SECRET` | >= 32 Zeichen (`openssl rand -base64 32`) |
| `AUTHENTIK_ISSUER` | OIDC-Issuer-URL der Authentik-Anwendung | | `AUTHENTIK_ISSUER` | OIDC-Issuer-URL der Authentik-Anwendung |
| `AUTHENTIK_CLIENT_ID` | Client-ID der Authentik-Anwendung | | `AUTHENTIK_CLIENT_ID` | Client-ID der Authentik-Anwendung |
| `AUTHENTIK_CLIENT_SECRET` | Client-Secret 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 | | `DATABASE_URL` | wird in Compose aus `POSTGRES_*` zusammengesetzt |
| `TRAEFIK_CERTRESOLVER` | Name des Traefik-Zertifikatsauflösers | | `TRAEFIK_CERTRESOLVER` | Name des Traefik-Zertifikatsauflösers |
| `TRAEFIK_NETWORK` | Name des externen Traefik-Netzes (Default `traefik`) | | `TRAEFIK_NETWORK` | Name des externen Traefik-Netzes (Default `frontend`) |
## Forwarded-Header & sichere Cookies ## Forwarded-Header & sichere Cookies
@@ -86,6 +90,10 @@ https://${APP_HOST}/api/auth/callback/authentik
Der Pfad `callback/authentik` entspricht dem NextAuth-Provider-Namen. Bei lokaler Der Pfad `callback/authentik` entspricht dem NextAuth-Provider-Namen. Bei lokaler
Entwicklung zusätzlich `http://localhost:3000/api/auth/callback/authentik`. 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 ## Health-Check & Middleware-Allowlist
`GET /api/health` ist **öffentlich** (anonym `200`, nur Liveness, keine `GET /api/health` ist **öffentlich** (anonym `200`, nur Liveness, keine
@@ -107,7 +115,7 @@ App-Healthcheck pingen `http://127.0.0.1:3000/api/health`.
```bash ```bash
cp .env.example .env # Werte setzen (APP_HOST, AUTH_*, AUTHENTIK_*, POSTGRES_*) cp .env.example .env # Werte setzen (APP_HOST, AUTH_*, AUTHENTIK_*, POSTGRES_*)
docker network create traefik # einmalig, falls nicht vorhanden docker network create frontend # einmalig, falls nicht vorhanden
make data # einmalig: OSRM-Geodaten vorbereiten (groß, dauert) make data # einmalig: OSRM-Geodaten vorbereiten (groß, dauert)
make deploy # build + up make deploy # build + up
``` ```