Files
Florian-netz/docker-compose.yml
Matthias Hochmeister d50ec765ab Workstream 10: Deployment (Docker + externes Traefik) (Phase 7)
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>
2026-06-09 12:35:45 +02:00

147 lines
4.7 KiB
YAML

# FlorianNetz — Basis-Compose hinter EXTERNEM Traefik.
#
# 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
#
# 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).
#
# 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
services:
app:
build:
context: .
dockerfile: Dockerfile
depends_on:
postgres:
condition: service_healthy
environment:
NODE_ENV: production
DATABASE_URL: postgres://${POSTGRES_USER:-floriannetz}:${POSTGRES_PASSWORD:-floriannetz}@postgres:5432/${POSTGRES_DB:-floriannetz}
# Forwarded-Header + sichere Cookies hinter Traefik.
AUTH_TRUST_HOST: "true"
AUTH_URL: https://${APP_HOST}
AUTH_SECRET: ${AUTH_SECRET}
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER}
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET}
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
- internal
healthcheck:
test:
- CMD-SHELL
- "wget -q -O - http://127.0.0.1:3000/api/health | grep -q ok"
interval: 30s
timeout: 5s
retries: 5
start_period: 40s
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.docker.network=${TRAEFIK_NETWORK:-traefik}"
- "traefik.http.routers.floriannetz.rule=Host(`${APP_HOST}`)"
- "traefik.http.routers.floriannetz.entrypoints=websecure"
- "traefik.http.routers.floriannetz.tls=true"
- "traefik.http.routers.floriannetz.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}"
- "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"
- "traefik.http.middlewares.floriannetz-sechdrs.headers.stsSeconds=63072000"
- "traefik.http.middlewares.floriannetz-sechdrs.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.floriannetz-sechdrs.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.floriannetz-sechdrs.headers.frameDeny=true"
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-floriannetz}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-floriannetz}
POSTGRES_DB: ${POSTGRES_DB:-floriannetz}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test:
- CMD-SHELL
- "pg_isready -U ${POSTGRES_USER:-floriannetz} -d ${POSTGRES_DB:-floriannetz}"
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
osrm:
build:
context: .
dockerfile: docker/osrm/Dockerfile
command: osrm-routed --algorithm mld /data/austria-latest.osrm
volumes:
- osrm-data:/data
networks:
- internal
expose:
- "5000"
healthcheck:
test:
- CMD-SHELL
- >-
wget -q -O - 'http://localhost:5000/table/v1/driving/15.6229,48.2079;16.3738,48.2082?sources=0'
| grep -q '"code":"Ok"'
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
restart: unless-stopped
nominatim:
image: mediagis/nominatim:4.4
environment:
PBF_URL: https://download.geofabrik.de/europe/austria-latest.osm.pbf
REPLICATION_URL: https://download.geofabrik.de/europe/austria-updates/
IMPORT_STYLE: address
NOMINATIM_PASSWORD: ${NOMINATIM_PASSWORD:-nominatim}
volumes:
- nominatim-data:/var/lib/postgresql/14/main
shm_size: 1g
networks:
- internal
expose:
- "8080"
healthcheck:
test:
- CMD-SHELL
- "wget -q -O - 'http://localhost:8080/status' | grep -q OK"
interval: 30s
timeout: 5s
retries: 5
start_period: 120s
restart: unless-stopped
volumes:
postgres-data:
osrm-data:
nominatim-data:
networks:
# Externes, von der separaten Traefik-Instanz verwaltetes Netz.
traefik:
external: true
name: ${TRAEFIK_NETWORK:-traefik}
# Internes Netz: Postgres/Geo sind nur app-intern erreichbar, nicht öffentlich.
internal:
internal: true