Terminkalender mit Serienterminen, Selbstanmeldung, Warteliste und Anwesenheitsliste. Vier Zustellwege hinter Schnittstellen: echtes SMTP, ntfy (selbst gehostet), Telegram und Web-Push. Kalenderabonnement als ICS-Feed, digitaler Dienstausweis mit signiertem QR und Pruefseite. Niemand wird ungefragt benachrichtigt: ein neuer Zustellweg hat eine leere Ereignisliste, und der Seed legt bewusst keine an. Was das Netz der Wehr verlaesst, bekommt nur die Kurzfassung ohne Namen und Details. Kein CalDAV (Lesen genuegt, ICS kann jeder Kalender) und kein WhatsApp (Verarbeitung ueber Meta, Kosten je Konversation, Telefonbuchabgleich) -- beides im README begruendet. 56 neue Unit-Tests, 17 neue E2E-Tests, Version 0.8.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
446 lines
17 KiB
YAML
446 lines
17 KiB
YAML
# =============================================================================
|
|
# FLORIAN — Produktionsnaher Stack.
|
|
# Für die Entwicklung wird docker-compose.override.yml automatisch daraufgelegt;
|
|
# die veröffentlicht die Dienste direkt auf localhost und braucht kein Traefik.
|
|
# Produktion starten: docker compose -f docker-compose.yml up -d
|
|
# Entwicklung starten: docker compose up -d
|
|
# =============================================================================
|
|
|
|
name: florian
|
|
|
|
x-app-env: &app-env
|
|
NODE_ENV: ${NODE_ENV:-production}
|
|
TZ: ${TZ:-Europe/Berlin}
|
|
APP_URL: ${APP_URL:-http://localhost:3000}
|
|
DATABASE_URL: ${DATABASE_URL}
|
|
DIRECT_DATABASE_URL: ${DIRECT_DATABASE_URL}
|
|
REDIS_URL: ${REDIS_URL}
|
|
WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-5}
|
|
S3_ENDPOINT: ${S3_ENDPOINT:-minio}
|
|
S3_PORT: ${S3_PORT:-9000}
|
|
S3_USE_SSL: ${S3_USE_SSL:-false}
|
|
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
|
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
|
S3_REGION: ${S3_REGION:-eu-central-1}
|
|
S3_BUCKET: ${S3_BUCKET:-florian}
|
|
S3_PUBLIC_URL: ${S3_PUBLIC_URL}
|
|
S3_URL_TTL: ${S3_URL_TTL:-900}
|
|
GOTENBERG_URL: ${GOTENBERG_URL:-http://gotenberg:3000}
|
|
GOTENBERG_TIMEOUT: ${GOTENBERG_TIMEOUT:-30}
|
|
AUTH_SECRET: ${AUTH_SECRET}
|
|
AUTH_TRUST_HOST: ${AUTH_TRUST_HOST:-true}
|
|
AUTH_SESSION_MAX_AGE: ${AUTH_SESSION_MAX_AGE:-28800}
|
|
AUTH_OIDC_ENABLED: ${AUTH_OIDC_ENABLED:-false}
|
|
AUTH_OIDC_ISSUER: ${AUTH_OIDC_ISSUER:-}
|
|
AUTH_OIDC_CLIENT_ID: ${AUTH_OIDC_CLIENT_ID:-}
|
|
AUTH_OIDC_CLIENT_SECRET: ${AUTH_OIDC_CLIENT_SECRET:-}
|
|
AUTH_OIDC_NAME: ${AUTH_OIDC_NAME:-authentik}
|
|
AUTH_OIDC_GROUP_CLAIM: ${AUTH_OIDC_GROUP_CLAIM:-groups}
|
|
AUTH_OIDC_GROUP_MAPPING: ${AUTH_OIDC_GROUP_MAPPING:-}
|
|
AUTH_OIDC_AUTO_PROVISION: ${AUTH_OIDC_AUTO_PROVISION:-true}
|
|
AUTH_OIDC_STANDARD_ORGANISATION: ${AUTH_OIDC_STANDARD_ORGANISATION:-}
|
|
AUTH_LOCAL_ENABLED: ${AUTH_LOCAL_ENABLED:-true}
|
|
AUTH_BCRYPT_ROUNDS: ${AUTH_BCRYPT_ROUNDS:-12}
|
|
KIOSK_ENABLED: ${KIOSK_ENABLED:-true}
|
|
KIOSK_DEVICE_SECRET: ${KIOSK_DEVICE_SECRET}
|
|
KIOSK_PIN_LAENGE: ${KIOSK_PIN_LAENGE:-6}
|
|
KIOSK_MAX_FEHLVERSUCHE: ${KIOSK_MAX_FEHLVERSUCHE:-5}
|
|
KIOSK_SPERRDAUER: ${KIOSK_SPERRDAUER:-300}
|
|
KIOSK_IDLE_TIMEOUT: ${KIOSK_IDLE_TIMEOUT:-120}
|
|
KARTE_STIL_URL: ${KARTE_STIL_URL:-}
|
|
KARTE_BREITENGRAD: ${KARTE_BREITENGRAD:-48.5}
|
|
KARTE_LAENGENGRAD: ${KARTE_LAENGENGRAD:-12.15}
|
|
KARTE_ZOOM: ${KARTE_ZOOM:-13}
|
|
ATEMSCHUTZ_HINWEIS: ${ATEMSCHUTZ_HINWEIS:-true}
|
|
ATEMSCHUTZ_FREIGABE: ${ATEMSCHUTZ_FREIGABE:-}
|
|
ATEMSCHUTZ_ABFRAGE_INTERVALL: ${ATEMSCHUTZ_ABFRAGE_INTERVALL:-600}
|
|
ATEMSCHUTZ_WARNDRUCK: ${ATEMSCHUTZ_WARNDRUCK:-55}
|
|
ATEMSCHUTZ_ZUSCHLAG: ${ATEMSCHUTZ_ZUSCHLAG:-10}
|
|
WETTER_ENABLED: ${WETTER_ENABLED:-false}
|
|
DWD_WARNCELL: ${DWD_WARNCELL:-}
|
|
DWD_WARNUNGEN_URL: ${DWD_WARNUNGEN_URL:-https://www.dwd.de/DWD/warnungen/warnapp/json/warnings.json}
|
|
WETTER_TIMEOUT: ${WETTER_TIMEOUT:-5}
|
|
WETTER_ZWISCHENSPEICHER: ${WETTER_ZWISCHENSPEICHER:-300}
|
|
MAIL_ENABLED: ${MAIL_ENABLED:-false}
|
|
MAIL_HOST: ${MAIL_HOST:-}
|
|
MAIL_PORT: ${MAIL_PORT:-587}
|
|
MAIL_SECURE: ${MAIL_SECURE:-false}
|
|
MAIL_USER: ${MAIL_USER:-}
|
|
MAIL_PASSWORD: ${MAIL_PASSWORD:-}
|
|
MAIL_FROM: ${MAIL_FROM:-}
|
|
TELEGRAM_ENABLED: ${TELEGRAM_ENABLED:-false}
|
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
|
NTFY_ENABLED: ${NTFY_ENABLED:-false}
|
|
NTFY_URL: ${NTFY_URL:-}
|
|
NTFY_TOKEN: ${NTFY_TOKEN:-}
|
|
NTFY_TIMEOUT: ${NTFY_TIMEOUT:-10}
|
|
WEBPUSH_ENABLED: ${WEBPUSH_ENABLED:-false}
|
|
WEBPUSH_PUBLIC_KEY: ${WEBPUSH_PUBLIC_KEY:-}
|
|
WEBPUSH_PRIVATE_KEY: ${WEBPUSH_PRIVATE_KEY:-}
|
|
WEBPUSH_KONTAKT: ${WEBPUSH_KONTAKT:-}
|
|
BENACHRICHTIGUNG_LAUF: ${BENACHRICHTIGUNG_LAUF:-0 7 * * *}
|
|
TERMIN_ERINNERUNG_TAGE: ${TERMIN_ERINNERUNG_TAGE:-7,1}
|
|
BENACHRICHTIGUNG_VERSUCHE: ${BENACHRICHTIGUNG_VERSUCHE:-3}
|
|
BENACHRICHTIGUNG_AUFBEWAHRUNG_TAGE: ${BENACHRICHTIGUNG_AUFBEWAHRUNG_TAGE:-90}
|
|
LLM_ENABLED: ${LLM_ENABLED:-false}
|
|
LLM_BASE_URL: ${LLM_BASE_URL:-}
|
|
LLM_API_KEY: ${LLM_API_KEY:-}
|
|
LLM_MODELL: ${LLM_MODELL:-}
|
|
GEOCODING_ENABLED: ${GEOCODING_ENABLED:-false}
|
|
NOMINATIM_URL: ${NOMINATIM_URL:-}
|
|
GEOCODING_TIMEOUT: ${GEOCODING_TIMEOUT:-5}
|
|
GEOCODING_LAENDER: ${GEOCODING_LAENDER:-de,at,ch}
|
|
PORTAL_ENABLED: ${PORTAL_ENABLED:-true}
|
|
PORTAL_GUELTIGKEIT_STUNDEN: ${PORTAL_GUELTIGKEIT_STUNDEN:-24}
|
|
PORTAL_MAX_ZUGRIFFE: ${PORTAL_MAX_ZUGRIFFE:-120}
|
|
WARTUNG_NACHTLAUF: ${WARTUNG_NACHTLAUF:-15 3 * * *}
|
|
WARTUNG_ERINNERUNG_LAUF: ${WARTUNG_ERINNERUNG_LAUF:-45 3 * * *}
|
|
WARTUNG_PLANUNGSHORIZONT_TAGE: ${WARTUNG_PLANUNGSHORIZONT_TAGE:-180}
|
|
AUFBEWAHRUNG_JAHRE: ${AUFBEWAHRUNG_JAHRE:-10}
|
|
AUDIT_AUFBEWAHRUNG_TAGE: ${AUDIT_AUFBEWAHRUNG_TAGE:-3650}
|
|
PERSON_ANONYMISIERUNG_TAGE: ${PERSON_ANONYMISIERUNG_TAGE:-365}
|
|
NEXT_TELEMETRY_DISABLED: "1"
|
|
|
|
x-logging: &logging
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "5"
|
|
|
|
services:
|
|
# ---------------------------------------------------------------------------
|
|
# Reverse Proxy. Terminiert TLS, verteilt auf App und MinIO.
|
|
# ---------------------------------------------------------------------------
|
|
traefik:
|
|
image: traefik:v3.3
|
|
container_name: florian-traefik
|
|
restart: unless-stopped
|
|
command:
|
|
- --providers.docker=true
|
|
- --providers.docker.exposedByDefault=false
|
|
- --providers.docker.network=florian-edge
|
|
- --entrypoints.web.address=:80
|
|
- --entrypoints.web.http.redirections.entryPoint.to=websecure
|
|
- --entrypoints.web.http.redirections.entryPoint.scheme=https
|
|
- --entrypoints.websecure.address=:443
|
|
- --certificatesresolvers.le.acme.email=${ACME_EMAIL}
|
|
- --certificatesresolvers.le.acme.storage=/acme/acme.json
|
|
- --certificatesresolvers.le.acme.tlschallenge=true
|
|
# Kein anonymes Nutzungs-Reporting.
|
|
- --global.sendAnonymousUsage=false
|
|
- --global.checkNewVersion=false
|
|
- --log.level=INFO
|
|
- --accesslog=true
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
volumes:
|
|
# Nur lesend — Traefik braucht die Socket-Verbindung zur Diensterkennung.
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
- traefik-acme:/acme
|
|
networks:
|
|
- edge
|
|
logging: *logging
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PostgreSQL 17 mit PostGIS. Einziger Ort für relationale Daten.
|
|
# ---------------------------------------------------------------------------
|
|
postgres:
|
|
image: postgis/postgis:17-3.5
|
|
container_name: florian-postgres
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-florian}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
POSTGRES_DB: ${POSTGRES_DB:-florian}
|
|
POSTGRES_APP_PASSWORD: ${POSTGRES_APP_PASSWORD}
|
|
# Deutsche Sortierung ueber ICU — das Debian-Basisimage hat keine
|
|
# de_DE-Locale erzeugt, ICU liefert sie unabhaengig vom Betriebssystem.
|
|
POSTGRES_INITDB_ARGS: "--locale-provider=icu --icu-locale=de-DE --locale=C.UTF-8 --encoding=UTF8"
|
|
TZ: ${TZ:-Europe/Berlin}
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
# Legt beim ERSTEN Start den RLS-pflichtigen Anwendungsbenutzer an.
|
|
- ./docker/postgres:/docker-entrypoint-initdb.d:ro
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-florian} -d ${POSTGRES_DB:-florian}"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 30s
|
|
networks:
|
|
- intern
|
|
logging: *logging
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Redis. Warteschlangen (BullMQ) und Rate-Limiting. Keine Nutzdaten.
|
|
# ---------------------------------------------------------------------------
|
|
redis:
|
|
image: redis:7-alpine
|
|
container_name: florian-redis
|
|
restart: unless-stopped
|
|
command:
|
|
- redis-server
|
|
- --requirepass
|
|
- ${REDIS_PASSWORD}
|
|
- --appendonly
|
|
- "yes"
|
|
- --maxmemory-policy
|
|
- noeviction
|
|
volumes:
|
|
- redis-data:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 10s
|
|
networks:
|
|
- intern
|
|
logging: *logging
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MinIO. Ablage ALLER Dateien — Fotos, Prüfprotokolle, Rechnungen, Exporte.
|
|
# In Produktion auf ein festes RELEASE-Tag pinnen.
|
|
# ---------------------------------------------------------------------------
|
|
minio:
|
|
image: minio/minio:latest
|
|
container_name: florian-minio
|
|
restart: unless-stopped
|
|
command: server /data --console-address ":9001"
|
|
environment:
|
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
|
MINIO_BROWSER_REDIRECT_URL: https://${MINIO_CONSOLE_DOMAIN}
|
|
MINIO_SERVER_URL: https://${MINIO_DOMAIN}
|
|
MINIO_UPDATE: "off"
|
|
TZ: ${TZ:-Europe/Berlin}
|
|
volumes:
|
|
- minio-data:/data
|
|
healthcheck:
|
|
test: ["CMD", "mc", "ready", "local"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 20s
|
|
networks:
|
|
- intern
|
|
- edge
|
|
labels:
|
|
- traefik.enable=true
|
|
# S3-API
|
|
- traefik.http.routers.minio-api.rule=Host(`${MINIO_DOMAIN}`)
|
|
- traefik.http.routers.minio-api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}
|
|
- traefik.http.routers.minio-api.tls.certresolver=le
|
|
- traefik.http.routers.minio-api.service=minio-api
|
|
- traefik.http.services.minio-api.loadbalancer.server.port=9000
|
|
# Weboberfläche
|
|
- traefik.http.routers.minio-console.rule=Host(`${MINIO_CONSOLE_DOMAIN}`)
|
|
- traefik.http.routers.minio-console.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}
|
|
- traefik.http.routers.minio-console.tls.certresolver=le
|
|
- traefik.http.routers.minio-console.service=minio-console
|
|
- traefik.http.services.minio-console.loadbalancer.server.port=9001
|
|
logging: *logging
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Einmaliger Job: legt den Bucket an und setzt ihn auf privat.
|
|
# ---------------------------------------------------------------------------
|
|
minio-init:
|
|
image: minio/mc:latest
|
|
container_name: florian-minio-init
|
|
depends_on:
|
|
minio:
|
|
condition: service_healthy
|
|
entrypoint: /bin/sh
|
|
command:
|
|
- -c
|
|
- |
|
|
mc alias set florian http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}" &&
|
|
mc mb --ignore-existing florian/"$${S3_BUCKET}" &&
|
|
mc anonymous set none florian/"$${S3_BUCKET}" &&
|
|
mc version enable florian/"$${S3_BUCKET}" &&
|
|
echo "Bucket $${S3_BUCKET} bereit."
|
|
environment:
|
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
|
S3_BUCKET: ${S3_BUCKET:-florian}
|
|
networks:
|
|
- intern
|
|
restart: "no"
|
|
logging: *logging
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Gotenberg. Rendert HTML zu PDF (Prüfprotokolle, Berichte).
|
|
# ---------------------------------------------------------------------------
|
|
gotenberg:
|
|
image: gotenberg/gotenberg:8
|
|
container_name: florian-gotenberg
|
|
restart: unless-stopped
|
|
command:
|
|
- gotenberg
|
|
- --chromium-disable-javascript=false
|
|
- --chromium-allow-list=file:///tmp/.*
|
|
- --api-timeout=${GOTENBERG_TIMEOUT:-30}s
|
|
environment:
|
|
TZ: ${TZ:-Europe/Berlin}
|
|
networks:
|
|
- intern
|
|
logging: *logging
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Kartenserver (Phase 6).
|
|
#
|
|
# Nur mit Profil: "docker compose --profile karte up -d". FLORIAN läuft ohne
|
|
# ihn — die Karte zeigt dann einen leeren Hintergrund, Hydranten und
|
|
# Lagekarte funktionieren trotzdem.
|
|
#
|
|
# Gebraucht wird eine MBTiles-Datei des Ausrückegebiets unter
|
|
# docker/karten/. Woher man sie bekommt, steht in der README; sie liegt
|
|
# bewusst nicht im Repository — das sind je nach Zuschnitt hunderte Megabyte
|
|
# bis Gigabyte, und jede Wehr braucht einen anderen Ausschnitt.
|
|
#
|
|
# Der Dienst hängt im internen Netz und im Edge-Netz: der Browser des
|
|
# Anwenders holt die Kacheln direkt, nicht über die Anwendung.
|
|
# ---------------------------------------------------------------------------
|
|
tileserver:
|
|
image: maptiler/tileserver-gl:v4.11.1
|
|
container_name: florian-tileserver
|
|
restart: unless-stopped
|
|
profiles: [karte]
|
|
command: ["--port", "8080", "--public_url", "${KARTE_OEFFENTLICH:-http://localhost:8085/}"]
|
|
volumes:
|
|
- ./docker/karten:/data:ro
|
|
ports:
|
|
- "${KARTE_PORT:-8085}:8080"
|
|
networks:
|
|
- edge
|
|
- intern
|
|
logging: *logging
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ntfy — Push aufs Handy, ohne dass etwas das Haus verlässt (Phase 7).
|
|
#
|
|
# Nur mit Profil: docker compose --profile push up -d
|
|
#
|
|
# Warum das der interessanteste Push-Weg für eine Wehr ist: kein Konto, keine
|
|
# Telefonnummer, kein Telefonbuchabgleich. Man abonniert in der App ein
|
|
# Thema — einen frei gewählten Namen — und bekommt Meldungen darauf.
|
|
#
|
|
# Der Container ist winzig und speichert standardmäßig nichts dauerhaft:
|
|
# eine zugestellte Meldung ist erledigt. Wer sie nachlesen können will,
|
|
# schaltet den Cache ein (siehe ntfy-Dokumentation) — dann liegen die Texte
|
|
# allerdings auch dort und nicht nur in FLORIAN.
|
|
#
|
|
# ACHTUNG: Ein Thema ist nur so geheim wie sein Name. Wer ihn kennt, liest
|
|
# mit. Deshalb schlägt FLORIAN Namen mit Zufallsanteil vor, und deshalb
|
|
# steht NTFY_TOKEN bereit, falls der eigene ntfy Zugriffsschutz bekommt.
|
|
# ---------------------------------------------------------------------------
|
|
ntfy:
|
|
image: binwiederhier/ntfy:v2.11.0
|
|
container_name: florian-ntfy
|
|
restart: unless-stopped
|
|
profiles: [push]
|
|
command: ["serve"]
|
|
environment:
|
|
NTFY_BASE_URL: ${NTFY_OEFFENTLICH:-http://localhost:8086}
|
|
NTFY_LISTEN_HTTP: ":80"
|
|
# Keine Anmeldung nötig, um zu lesen und zu schreiben — geschützt ist
|
|
# das Ding durch die Unerratbarkeit der Themennamen und dadurch, dass es
|
|
# im eigenen Netz steht. Wer es ins Internet stellt, setzt
|
|
# NTFY_AUTH_DEFAULT_ACCESS auf "deny-all" und vergibt Token.
|
|
NTFY_AUTH_DEFAULT_ACCESS: ${NTFY_ZUGRIFF:-read-write}
|
|
NTFY_BEHIND_PROXY: "true"
|
|
ports:
|
|
- "${NTFY_PORT:-8086}:80"
|
|
networks:
|
|
- edge
|
|
- intern
|
|
logging: *logging
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Die Anwendung selbst (Next.js, Standalone-Build).
|
|
# Führt vor dem Start die ausstehenden Prisma-Migrationen aus.
|
|
# ---------------------------------------------------------------------------
|
|
app:
|
|
build:
|
|
context: .
|
|
dockerfile: docker/Dockerfile
|
|
target: runner
|
|
image: florian-app:${IMAGE_TAG:-latest}
|
|
container_name: florian-app
|
|
restart: unless-stopped
|
|
environment:
|
|
<<: *app-env
|
|
PORT: "3000"
|
|
HOSTNAME: "0.0.0.0"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
minio:
|
|
condition: service_healthy
|
|
networks:
|
|
- intern
|
|
- edge
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.app.rule=Host(`${APP_DOMAIN}`)
|
|
- traefik.http.routers.app.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}
|
|
- traefik.http.routers.app.tls.certresolver=le
|
|
- traefik.http.services.app.loadbalancer.server.port=3000
|
|
logging: *logging
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Hintergrundjobs: Fristenberechnung, Erinnerungen, PDF-Erzeugung, Exporte.
|
|
# Gleiches Image wie die App, anderer Einstiegspunkt.
|
|
# ---------------------------------------------------------------------------
|
|
worker:
|
|
build:
|
|
context: .
|
|
dockerfile: docker/Dockerfile
|
|
target: runner
|
|
image: florian-app:${IMAGE_TAG:-latest}
|
|
container_name: florian-worker
|
|
restart: unless-stopped
|
|
# --conditions=react-server: lib/storage.ts und lib/pdf.ts sichern sich mit
|
|
# dem Paket "server-only" ab. Unter node wirft dessen Standardexport;
|
|
# mit dieser Auflösungsbedingung greift der leere react-server-Export,
|
|
# und der Worker darf dieselben Bausteine benutzen wie die Anwendung.
|
|
command: ["node", "--enable-source-maps", "--conditions=react-server", "dist/worker/index.js"]
|
|
environment:
|
|
<<: *app-env
|
|
ROLLE: worker
|
|
healthcheck:
|
|
test: ["CMD", "node", "-e", "process.exit(0)"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 20s
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
app:
|
|
condition: service_started
|
|
networks:
|
|
- intern
|
|
logging: *logging
|
|
|
|
volumes:
|
|
postgres-data:
|
|
redis-data:
|
|
minio-data:
|
|
traefik-acme:
|
|
|
|
networks:
|
|
# Nur was von außen erreichbar sein muss, hängt hier drin.
|
|
edge:
|
|
name: florian-edge
|
|
# Datenbank, Redis, Gotenberg: intern, kein Zugang von außen.
|
|
intern:
|
|
name: florian-intern
|
|
internal: false
|