Die Anleitung setzt jetzt voraus, was ohnehin da ist: ein Server mit laufendem Docker und ein Reverse Proxy, der schon steht - auch auf einem anderen Rechner. Docker-Installation und Betriebssystemhaertung sind raus, dafuer ist der Proxy-Abschnitt der ausfuehrlichste geworden. Damit ein externer Proxy die Anwendung ueberhaupt erreicht, war eine Aenderung noetig: Die Bindeadresse stand fest auf 127.0.0.1. Jetzt gibt es APP_BIND, Standard bleibt die Loopback-Adresse. Der Weg dorthin ist nach Vorzug sortiert - privates Netz, dann WireGuard, dann als Notloesung oeffentliche Adresse mit Firewallregel. Zwischen Proxy und Anwendung laeuft unverschluesseltes HTTP; das gehoert nicht ins offene Netz. Deshalb steht in der Kontrolle eine Gegenprobe, die fehlschlagen MUSS: Antwortet der Anwendungsport von aussen, ist die Firewallregel falsch. Fertige Konfigurationen fuer Caddy, nginx und Traefik, jeweils mit den Werten, die sonst uebersehen werden: - Zeitgrenze 10 Minuten - sonst brechen Belegmappe, Rechnungslegung und Vollexport mitten im Erzeugen ab. Die Standardwerte von nginx liegen bei 60 Sekunden. - Antwort nicht puffern - der Vollexport kommt als Strom. Puffert der Proxy, wartet der Browser, bis alles im Speicher des Proxys liegt, und laeuft dabei in die Zeitgrenze. - Und der Hinweis, dass der Proxy KEINE eigene CSP setzen darf: Sie enthaelt eine Nonce, die pro Anfrage neu ist. Eine feste CSP blockiert die eigenen Skripte - die Seite laedt dann, laesst sich aber nicht bedienen. Der A-Record zeigt auf den Proxy-Server, nicht auf den Anwendungsserver. Stand vorher falsch da. Etappe: 8 Phase: Haertung
174 lines
6.6 KiB
YAML
174 lines
6.6 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.
|
|
- '127.0.0.1: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'
|
|
ENC_KEY_V1: ${ENC_KEY_V1:?ENC_KEY_V1 fehlt in der .env - ohne ihn sind die verschluesselten Felder nicht lesbar}
|
|
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}
|
|
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}
|
|
TZ: Europe/Berlin
|
|
volumes:
|
|
- ./backups:/backups
|
|
- ./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
|