diff --git a/.env.example b/.env.example index 8e338bf..ef9f640 100644 --- a/.env.example +++ b/.env.example @@ -31,11 +31,13 @@ HAVERSINE_KMH=50 # Deployment / externes Traefik # APP_HOST ist der öffentliche Hostname (Traefik-Routing + AUTH_URL-Basis). # 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_CERTRESOLVER=letsencrypt -# Name des externen, von Traefik verwalteten Docker-Netzes. -TRAEFIK_NETWORK=traefik +# Name des externen, von Traefik verwalteten Docker-Netzes +# (im feuerwehr_dashboard heißt es "frontend"). Muss existieren: +# docker network create frontend +TRAEFIK_NETWORK=frontend # Optionaler Katalog-Seed beim Container-Start (idempotent). RUN_SEED=false # Postgres-Zugangsdaten für den Compose-Postgres-Service. diff --git a/Makefile b/Makefile index bcc1b56..604d57b 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ # make build-app migrate # # Voll-Deploy hinter externem Traefik (Docker): -# docker network create traefik # einmalig +# docker network create frontend # einmalig (externes Traefik-Netz) # make deploy # # `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" # --- 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 build: ## App-Image bauen (Next.js standalone, non-root) $(COMPOSE) build app diff --git a/docker-compose.yml b/docker-compose.yml index d101f6f..1b4c36c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,22 @@ # 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 -# eine separat betriebene Traefik-Instanz, die am externen Netz "${TRAEFIK_NETWORK}" -# (Default: traefik) lauscht. Dieses Netz muss bereits existieren: -# docker network create traefik +# die separat betriebene Traefik-Instanz am Netz "${TRAEFIK_NETWORK}" (Default: +# frontend). Dieses Netz muss bereits existieren: +# docker network create frontend # -# Geo-Dienste (osrm, nominatim) sind hier mit ihren Laufzeit-Verträgen definiert; -# das schwergewichtige Daten-Preprocessing/Volume kommt aus docker-compose.geo.yml -# (siehe scripts/prepare-osm-data.sh / infra/geo). +# Postgres/Geo liegen am internen Bridge-Netz (keine veröffentlichten Ports, +# also nicht öffentlich erreichbar) — der App-Container hat über dieses Netz +# zugleich Egress (z. B. für den Authentik-OIDC-Token-Austausch). # -# Start: -# docker compose --env-file .env up -d -# Lokal ohne Traefik/TLS: -# docker compose -f docker-compose.yml -f docker-compose.override.yml up -d +# Start: docker compose --env-file .env up -d +# Lokal: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d services: app: @@ -32,13 +36,14 @@ services: AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER} AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID} AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET} + AUTHENTIK_ADMIN_GROUP: ${AUTHENTIK_ADMIN_GROUP:-floriannetz-admins} OSRM_URL: http://osrm:5000 NOMINATIM_URL: http://nominatim:8080 GEO_HTTP_TIMEOUT_MS: ${GEO_HTTP_TIMEOUT_MS:-4000} HAVERSINE_KMH: ${HAVERSINE_KMH:-50} RUN_SEED: ${RUN_SEED:-false} networks: - - traefik + - frontend - internal healthcheck: test: @@ -51,11 +56,12 @@ services: restart: unless-stopped labels: - "traefik.enable=true" - - "traefik.docker.network=${TRAEFIK_NETWORK:-traefik}" - - "traefik.http.routers.floriannetz.rule=Host(`${APP_HOST}`)" + - "traefik.docker.network=${TRAEFIK_NETWORK:-frontend}" - "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.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}" + - "traefik.http.routers.floriannetz.service=floriannetz" - "traefik.http.services.floriannetz.loadbalancer.server.port=3000" # Security-Header-Middleware (zusätzlich zu next.config.ts; defense-in-depth). - "traefik.http.routers.floriannetz.middlewares=floriannetz-sechdrs" @@ -137,10 +143,12 @@ volumes: nominatim-data: networks: - # Externes, von der separaten Traefik-Instanz verwaltetes Netz. - traefik: + # Externes, von der separaten Traefik-Instanz verwaltetes Netz (wie im + # feuerwehr_dashboard "frontend"). Muss existieren: docker network create frontend + frontend: external: true - name: ${TRAEFIK_NETWORK:-traefik} - # Internes Netz: Postgres/Geo sind nur app-intern erreichbar, nicht öffentlich. + name: ${TRAEFIK_NETWORK:-frontend} + # Internes Bridge-Netz: Postgres/Geo ohne veröffentlichte Ports (nicht + # öffentlich), zugleich Egress für den App-Container (Authentik-OIDC). internal: - internal: true + driver: bridge diff --git a/docs/reference/deployment-traefik.md b/docs/reference/deployment-traefik.md index 7db21fc..c288463 100644 --- a/docs/reference/deployment-traefik.md +++ b/docs/reference/deployment-traefik.md @@ -18,17 +18,20 @@ Der Stack besteht aus genau vier Services (kein Proxy): 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. +- **`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 traefik +docker network create frontend ``` Die externe Traefik-Instanz muss: @@ -36,7 +39,7 @@ 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`). +- 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 @@ -48,16 +51,17 @@ Vollständiger Vertrag in `.env.example`. Für den Betrieb hinter Traefik zwinge | 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_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 `traefik`) | +| `TRAEFIK_NETWORK` | Name des externen Traefik-Netzes (Default `frontend`) | ## 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 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 @@ -107,7 +115,7 @@ App-Healthcheck pingen `http://127.0.0.1:3000/api/health`. ```bash 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 deploy # build + up ```