compose.yaml aus docs/11-betrieb.md, ergänzt um Healthchecks für db-backup und offsite-backup. Das Dokument verlangt für jeden Dienst einen, die abgedruckte Fassung hatte für die beiden keinen, und ohne ihn zeigt Dockge nur "läuft", auch wenn seit Wochen nichts mehr gesichert wird. Die Migration läuft im Startkommando von web, nicht in einem Init-Container. Die Prisma-CLI samt Schema-Engine liegt dafür unter /app/migrator im Image: ein Container, der nur startet, wenn gerade eine Registry erreichbar ist, wäre auf einem Homeserver die falsche Bauart. Alle drei Images gebaut und laufen gegen eine echte Postgres-Instanz geprüft: web wendet Migrationen an und liefert /api/health aus, worker schreibt sein Lebenszeichen, pdf rendert ein echtes PDF. Die CI prüft Lint, Typen, Tests und E2E gegen den Standalone-Server, also so, wie der Container startet, und hält das Bündel-Budget aus docs/09-performance-und-ux.md ein. Phase 0 aus TASKS.md ist damit abgehakt.
201 lines
5.7 KiB
YAML
201 lines
5.7 KiB
YAML
# AfiS-Aktensystem — ein einziger Dockge-Stack.
|
|
# Ablage: /opt/stacks/afis/compose.yaml, .env im eigenen Dockge-Reiter.
|
|
# Begründung der Entscheidungen in docs/11-betrieb.md:
|
|
# - keine Einmal- und keine Init-Container, Dockge zeigt beendete Container nicht
|
|
# - Migrationen laufen im Startkommando von `web`
|
|
# - jeder Dienst hat einen Healthcheck, das ist in Dockge die Statusanzeige
|
|
# - kein Reverse Proxy: TLS und Kompression macht der Cloudflare Tunnel
|
|
#
|
|
# DEIN-USER durch den eigenen GitHub-Namen ersetzen.
|
|
|
|
x-logging: &logging
|
|
logging:
|
|
driver: json-file
|
|
options: { max-size: '10m', max-file: '3' }
|
|
|
|
services:
|
|
web:
|
|
<<: *logging
|
|
image: ghcr.io/DEIN-USER/afis-web:latest
|
|
restart: unless-stopped
|
|
# Die Migration läuft im Startkommando, nicht in einem Init-Container.
|
|
# Die Prisma-CLI liegt im Image unter /app/migrator, es wird nichts nachgeladen.
|
|
command: sh -c "cd /app/migrator && node node_modules/prisma/build/index.js migrate deploy && cd /app && node web/server.js"
|
|
env_file: .env
|
|
environment:
|
|
NODE_ENV: production
|
|
PORT: '3000'
|
|
HOSTNAME: '0.0.0.0'
|
|
volumes:
|
|
- files:/data/files
|
|
- clamav_socket:/var/run/clamav
|
|
ports:
|
|
- '3000:3000' # nur im LAN, von außen kommt der Tunnel
|
|
depends_on:
|
|
pgbouncer: { condition: service_healthy }
|
|
clamav: { condition: service_healthy }
|
|
healthcheck:
|
|
test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/api/health']
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 40s
|
|
|
|
worker:
|
|
<<: *logging
|
|
image: ghcr.io/DEIN-USER/afis-worker:latest
|
|
restart: unless-stopped
|
|
env_file: .env
|
|
depends_on:
|
|
pgbouncer: { condition: service_healthy }
|
|
healthcheck:
|
|
test: ['CMD', 'node', 'healthcheck.js'] # prüft Gateway-Verbindung
|
|
interval: 60s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
db:
|
|
<<: *logging
|
|
image: postgres:16-alpine
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_DB: afis
|
|
POSTGRES_USER: afis
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
TZ: ${TZ}
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'pg_isready -U afis -d afis']
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
pgbouncer:
|
|
<<: *logging
|
|
image: edoburu/pgbouncer:latest
|
|
restart: unless-stopped
|
|
environment:
|
|
DB_HOST: db
|
|
DB_USER: afis
|
|
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
|
DB_NAME: afis
|
|
POOL_MODE: transaction
|
|
MAX_CLIENT_CONN: '100'
|
|
DEFAULT_POOL_SIZE: '20'
|
|
AUTH_TYPE: scram-sha-256
|
|
depends_on:
|
|
db: { condition: service_healthy }
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -p 6432 -U afis']
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
clamav:
|
|
<<: *logging
|
|
image: clamav/clamav:stable
|
|
restart: unless-stopped
|
|
environment:
|
|
CLAMAV_NO_MILTERD: 'true'
|
|
FRESHCLAM_CHECKS: '2'
|
|
volumes:
|
|
- clamav_db:/var/lib/clamav
|
|
- clamav_socket:/var/run/clamav
|
|
- files:/data/files
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'clamdscan --ping 1 || exit 1']
|
|
interval: 60s
|
|
timeout: 15s
|
|
retries: 3
|
|
start_period: 300s # erster Signaturdownload dauert
|
|
|
|
pdf:
|
|
<<: *logging
|
|
image: ghcr.io/DEIN-USER/afis-pdf:latest # Playwright-Basis
|
|
restart: unless-stopped
|
|
environment:
|
|
PORT: '3000'
|
|
APP_URL: ${APP_URL}
|
|
healthcheck:
|
|
test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/health']
|
|
interval: 60s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
cloudflared:
|
|
<<: *logging
|
|
image: cloudflare/cloudflared:latest
|
|
restart: unless-stopped
|
|
command: tunnel --no-autoupdate run
|
|
environment:
|
|
TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN}
|
|
depends_on:
|
|
web: { condition: service_healthy }
|
|
healthcheck:
|
|
test: ['CMD', 'cloudflared', '--version']
|
|
interval: 60s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 20s
|
|
|
|
db-backup:
|
|
<<: *logging
|
|
image: prodrigestivill/postgres-backup-local:16
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_HOST: db
|
|
POSTGRES_DB: afis
|
|
POSTGRES_USER: afis
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
POSTGRES_EXTRA_OPTS: '-Fc'
|
|
SCHEDULE: '0 15 4 * * *' # täglich 04:15
|
|
BACKUP_KEEP_DAYS: '7'
|
|
BACKUP_KEEP_WEEKS: '4'
|
|
BACKUP_KEEP_MONTHS: '6'
|
|
HEALTHCHECK_PORT: '8080'
|
|
TZ: ${TZ}
|
|
volumes:
|
|
- /mnt/backup/afis/db:/backups
|
|
depends_on:
|
|
db: { condition: service_healthy }
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'wget -qO- http://localhost:8080/ || exit 1']
|
|
interval: 60s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
offsite-backup:
|
|
<<: *logging
|
|
image: mazzolino/restic:latest
|
|
restart: unless-stopped
|
|
environment:
|
|
RUN_ON_STARTUP: 'false'
|
|
BACKUP_CRON: '0 30 4 * * *' # 15 Minuten nach dem Dump
|
|
RESTIC_REPOSITORY: ${RESTIC_REPOSITORY}
|
|
RESTIC_PASSWORD: ${RESTIC_PASSWORD}
|
|
RESTIC_BACKUP_SOURCES: /data/db /data/files
|
|
RESTIC_FORGET_ARGS: '--keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune'
|
|
TZ: ${TZ}
|
|
volumes:
|
|
- /mnt/backup/afis/db:/data/db:ro
|
|
- files:/data/files:ro
|
|
# Kein HTTP-Endpunkt im Image. Geprüft wird, dass der Cron-Dauerläufer noch
|
|
# steht und restic aufrufbar ist. Ohne Healthcheck wäre der Dienst in Dockge
|
|
# nur "läuft", auch wenn er seit Wochen nichts mehr sichert.
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'pgrep -f go-crond >/dev/null && restic version >/dev/null || exit 1']
|
|
interval: 300s
|
|
timeout: 15s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
volumes:
|
|
pgdata:
|
|
files:
|
|
clamav_db:
|
|
clamav_socket:
|