Der Dienst "app" zaehlt seine Umgebungsvariablen einzeln auf; ein env_file gibt es nicht. Drei Werte, die die Anwendung ausliest, standen dort nicht: ERSTEINRICHTUNG_GEHEIMNIS, VERTRAUE_PROXY (beide seit v1.11.0) und S3_SSE (seit v1.11.1). Wer sie laut .env.example und INSTALLATION.md eintrug, brachte sie damit nicht in den Behaelter - die Anleitung war an dieser Stelle schlicht falsch. Besonders unangenehm bei VERTRAUE_PROXY: Der Wert fehlt still, die Anmeldesperre zaehlt dann nur nach E-Mail-Adresse statt zusaetzlich nach Anschluss, und im Betrieb faellt nichts auf. VERTRAUE_PROXY bekommt bewusst KEINE Vorgabe "true". Die strengere Einstellung bleibt die Voreinstellung; wer einen Proxy davorstellt, sagt es ausdruecklich. Ohne Proxy ist der weitergereichte Kopf frei erfunden. Der eigentliche Ertrag ist der Test: Er liest, welche Variablen env.ts ausliest - ueber process.env ebenso wie ueber pflicht(), zahl(), schalter() und schluessel32() -, vergleicht das mit dem, was die Compose-Datei dem Dienst mitgibt, und meldet jede Luecke. Ausnahmen sind aufgezaehlt und begruendet. Damit wiederholt sich der Fehler beim naechsten neuen Wert nicht: Wer ihn einfuehrt und das Durchreichen vergisst, sieht es sofort. Gegen drei Mutationen geprueft: Wert wieder entfernt, Vorgabe "true" gesetzt, S3_SSE entfernt - jede wird rot. Refs #43
202 lines
8.2 KiB
YAML
202 lines
8.2 KiB
YAML
# =============================================================================
|
|
# Betreuungs-Cockpit - Produktivbetrieb
|
|
# =============================================================================
|
|
#
|
|
# docker compose up -d --build
|
|
#
|
|
# Kein Reverse Proxy in dieser Datei: der ist bereits vorhanden. Die
|
|
# Anwendung beendet selbst kein TLS und horcht standardmaessig nur auf
|
|
# 127.0.0.1:3000.
|
|
#
|
|
# Liegt der Proxy auf einem anderen Server, setze APP_BIND in der .env auf
|
|
# die Adresse, ueber die er herankommt - siehe docs/INSTALLATION.md,
|
|
# Schritt 7.
|
|
#
|
|
# Die Datenbank hat bewusst KEINE Portfreigabe. Sie ist nur ueber das
|
|
# interne Docker-Netz erreichbar. Wer mit psql heran muss, geht ueber
|
|
# docker compose exec db psql -U cockpit betreuungs_cockpit
|
|
# =============================================================================
|
|
|
|
name: betreuungs-cockpit
|
|
|
|
services:
|
|
db:
|
|
image: postgres:17-alpine
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-cockpit}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in der .env}
|
|
POSTGRES_DB: ${POSTGRES_DB:-betreuungs_cockpit}
|
|
# Deutsche Sortierung und Zeichensatz - wichtig fuer Namenslisten
|
|
# und fuer die Volltextsuche mit der Konfiguration "german".
|
|
POSTGRES_INITDB_ARGS: '--encoding=UTF8 --locale=de_DE.UTF-8'
|
|
LANG: de_DE.UTF-8
|
|
volumes:
|
|
- postgres-daten:/var/lib/postgresql/data
|
|
- ./backups:/backups
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-cockpit} -d ${POSTGRES_DB:-betreuungs_cockpit}']
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 30s
|
|
networks: [intern]
|
|
# Kein "ports:" - die Datenbank ist von aussen nicht erreichbar.
|
|
|
|
minio:
|
|
# S3-kompatibler Objektspeicher fuer die Dokumente. Wer einen externen
|
|
# Speicher in der EU nutzt (z.B. Hetzner Object Storage), kann diesen
|
|
# Dienst weglassen und in der .env S3_ENDPOINT darauf zeigen lassen.
|
|
# Auch dann gilt: AV-Vertrag nach Art. 28 DSGVO abschliessen.
|
|
image: minio/minio:RELEASE.2025-01-20T14-49-07Z
|
|
restart: unless-stopped
|
|
command: server /daten --console-address ":9001"
|
|
environment:
|
|
MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:?S3_ACCESS_KEY_ID fehlt in der .env}
|
|
MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:?S3_SECRET_ACCESS_KEY fehlt in der .env}
|
|
MINIO_BROWSER_REDIRECT_URL: ${MINIO_KONSOLE_URL:-http://127.0.0.1:9001}
|
|
volumes:
|
|
- minio-daten:/daten
|
|
healthcheck:
|
|
test: ['CMD', 'mc', 'ready', 'local']
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 20s
|
|
ports:
|
|
# Nur an die Loopback-Adresse gebunden. Die Konsole erreichst du
|
|
# ueber einen SSH-Tunnel oder ueber deinen vorhandenen Proxy.
|
|
#
|
|
# Laeuft auf dem Server schon etwas auf 9001, weich in der .env aus:
|
|
# MINIO_KONSOLE_PORT=9011
|
|
- '${MINIO_KONSOLE_BIND:-127.0.0.1}:${MINIO_KONSOLE_PORT:-9001}:9001'
|
|
networks: [intern]
|
|
|
|
app:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
args:
|
|
# In der Oberflaeche und unter /api/gesundheit sichtbar. Setzen mit:
|
|
# GIT_COMMIT=$(git rev-parse --short HEAD) docker compose up -d --build
|
|
GIT_COMMIT: ${GIT_COMMIT:-}
|
|
BUILD_ZEIT: ${BUILD_ZEIT:-}
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
minio:
|
|
condition: service_healthy
|
|
environment:
|
|
NODE_ENV: production
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-cockpit}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-betreuungs_cockpit}?schema=public&connection_limit=10
|
|
AUTH_SECRET: ${AUTH_SECRET:?AUTH_SECRET fehlt in der .env}
|
|
AUTH_URL: ${AUTH_URL:?AUTH_URL fehlt in der .env}
|
|
AUTH_TRUST_HOST: 'true'
|
|
# Ohne diesen Wert laesst sich der erste Zugang nicht anlegen - siehe
|
|
# src/server/actions/ersteinrichtung.ts. Leer heisst: gesperrt.
|
|
ERSTEINRICHTUNG_GEHEIMNIS: ${ERSTEINRICHTUNG_GEHEIMNIS:-}
|
|
# Bewusst OHNE Vorgabe "true": Die strengere Einstellung bleibt die
|
|
# Voreinstellung. Wer einen Reverse Proxy davorstellt - der
|
|
# dokumentierte Betrieb tut das -, sagt es in der .env ausdruecklich.
|
|
# Ohne den Wert zaehlt die Anmeldesperre nur nach E-Mail-Adresse.
|
|
VERTRAUE_PROXY: ${VERTRAUE_PROXY:-}
|
|
VERTRAUE_PROXY_STUFEN: ${VERTRAUE_PROXY_STUFEN:-}
|
|
ENC_KEY_V1: ${ENC_KEY_V1:?ENC_KEY_V1 fehlt in der .env - ohne ihn sind die verschluesselten Felder nicht lesbar}
|
|
# Bleibt leer, solange nicht rotiert wird. Die Anwendung ueberspringt
|
|
# leere Schluesselvariablen - siehe src/lib/env.ts.
|
|
# ENC_KEY_V2: ${ENC_KEY_V2:-}
|
|
ENC_KEY_AKTIV: ${ENC_KEY_AKTIV:-1}
|
|
INDEX_KEY: ${INDEX_KEY:?INDEX_KEY fehlt in der .env}
|
|
SESSION_MAX_AGE: ${SESSION_MAX_AGE:-1800}
|
|
REAUTH_MAX_AGE: ${REAUTH_MAX_AGE:-300}
|
|
LOGIN_MAX_VERSUCHE: ${LOGIN_MAX_VERSUCHE:-5}
|
|
LOGIN_FENSTER_SEKUNDEN: ${LOGIN_FENSTER_SEKUNDEN:-900}
|
|
LOGIN_SPERRE_SEKUNDEN: ${LOGIN_SPERRE_SEKUNDEN:-900}
|
|
SPEICHER_TREIBER: s3
|
|
S3_ENDPOINT: ${S3_ENDPOINT:-http://minio:9000}
|
|
S3_REGION: ${S3_REGION:-eu-central}
|
|
S3_BUCKET: ${S3_BUCKET:-betreuungs-cockpit}
|
|
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
|
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
|
# Leer lassen. Das mitgelieferte MinIO laeuft ohne KMS und lehnt jede
|
|
# Ablage ab, sobald hier etwas steht - siehe .env.example.
|
|
S3_SSE: ${S3_SSE:-}
|
|
UPLOAD_MAX_BYTES: ${UPLOAD_MAX_BYTES:-26214400}
|
|
OCR_AKTIV: ${OCR_AKTIV:-true}
|
|
SMTP_HOST: ${SMTP_HOST:-}
|
|
SMTP_PORT: ${SMTP_PORT:-587}
|
|
SMTP_USER: ${SMTP_USER:-}
|
|
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
|
SMTP_FROM: ${SMTP_FROM:-}
|
|
TZ: Europe/Berlin
|
|
ports:
|
|
# Standard ist die Loopback-Adresse: dann erreicht die Anwendung nur
|
|
# ein Proxy, der auf demselben Rechner laeuft.
|
|
#
|
|
# Liegt der Reverse Proxy auf einem ANDEREN Server, muss hier die
|
|
# Adresse stehen, ueber die er herankommt - am besten die eines
|
|
# privaten Netzes oder eines WireGuard-Tunnels:
|
|
#
|
|
# APP_BIND=10.0.0.5
|
|
#
|
|
# 0.0.0.0 nur setzen, wenn eine Firewall den Port ausschliesslich
|
|
# fuer die IP des Proxy-Servers oeffnet. Die Anwendung beendet kein
|
|
# TLS und gehoert nie unverschluesselt ins offene Netz.
|
|
- '${APP_BIND:-127.0.0.1}:${APP_PORT:-3000}:3000'
|
|
networks: [intern]
|
|
healthcheck:
|
|
test: ['CMD', 'node', '-e', "fetch('http://127.0.0.1:3000/api/gesundheit').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 40s
|
|
|
|
sicherung:
|
|
# Taeglich um 03:15 Uhr ein verschluesseltes Backup nach ./backups.
|
|
# Legt zusaetzlich die Pruefsumme ab und raeumt alte Staende auf.
|
|
image: postgres:17-alpine
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
environment:
|
|
PGHOST: db
|
|
PGUSER: ${POSTGRES_USER:-cockpit}
|
|
PGPASSWORD: ${POSTGRES_PASSWORD}
|
|
PGDATABASE: ${POSTGRES_DB:-betreuungs_cockpit}
|
|
SICHERUNG_PASSWORT: ${SICHERUNG_PASSWORT:?SICHERUNG_PASSWORT fehlt in der .env}
|
|
SICHERUNG_AUFBEWAHRUNG_TAGE: ${SICHERUNG_AUFBEWAHRUNG_TAGE:-30}
|
|
SICHERUNG_DOKUMENTE_QUELLE: /dokumente
|
|
TZ: Europe/Berlin
|
|
volumes:
|
|
- ./backups:/backups
|
|
# Die Dokumente, ausdruecklich NUR LESEND.
|
|
#
|
|
# Der Sicherungsdienst laeuft unbeaufsichtigt per cron. Ein Dienst,
|
|
# der die Daten zerstoeren kann, die er sichern soll, ist ein Risiko
|
|
# ohne Gegenwert - ein Fehler im Skript oder ein kompromittierter
|
|
# Behaelter wuerde beides auf einmal treffen.
|
|
#
|
|
# Zurueckspielen geschieht deshalb nicht hierher, sondern in ein
|
|
# Verzeichnis unter ./backups; siehe scripts/wiederherstellung.sh.
|
|
- minio-daten:/dokumente:ro
|
|
- ./scripts/sicherung.sh:/sicherung.sh:ro
|
|
- ./scripts/wiederherstellung.sh:/wiederherstellung.sh:ro
|
|
entrypoint: ['/bin/sh', '-c']
|
|
command:
|
|
- |
|
|
apk add --no-cache openssl coreutils >/dev/null
|
|
echo "15 3 * * * /bin/sh /sicherung.sh >> /backups/sicherung.log 2>&1" > /etc/crontabs/root
|
|
echo "Sicherungsdienst bereit. Naechster Lauf 03:15 Uhr."
|
|
crond -f -l 8
|
|
networks: [intern]
|
|
|
|
volumes:
|
|
postgres-daten:
|
|
minio-daten:
|
|
|
|
networks:
|
|
intern:
|
|
driver: bridge
|