Files
AfiS-Aktensystem/compose.yaml
T
Harlekin 28886b3d4d
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Seed ueber den bin-Eintrag, Auslagerungs-Backup nur auf Anforderung
Zwei Fehler aus der ersten Inbetriebnahme.

Der Seed wurde ueber einen geratenen Pfad in tsx aufgerufen. Jetzt ueber
node_modules/.bin/tsx - der Eintrag stimmt unabhaengig davon, wie das Paket
seine Dateien anordnet.

Das Auslagerungs-Backup braucht RESTIC_REPOSITORY und RESTIC_PASSWORD und
beendet sich ohne beides sofort wieder, also im Sekundentakt neu. Es steht
jetzt wie der Tunnel in einem Profil und startet nur mit
`docker compose --profile offsite up -d`. Ohne Angabe laufen sieben Dienste.
2026-09-04 10:36:53 +02:00

249 lines
7.8 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
# - Migration und Seed laufen im Startkommando von `web`
# - jeder Dienst hat einen Healthcheck, das ist in Dockge die Statusanzeige
# - kein Reverse Proxy im Stack: TLS macht ein Proxy davor
#
# Die drei eigenen Images werden hier gebaut, nicht aus einer Registry gezogen:
# docker compose build && docker compose up -d
# Der Baukontext ist das Repository-Wurzelverzeichnis, der Stack-Ordner muss
# also den Quelltext enthalten.
#
# HINWEIS ZU DEN PORTS: `PORT` und die Healthchecks bleiben bei 3000 - das ist
# der Port *im* Container. Auf welchem Port die Anwendung auf dem Host
# erreichbar ist, steuert WEB_PORT in der .env.
x-logging: &logging
logging:
driver: json-file
options: { max-size: '10m', max-file: '3' }
services:
web:
<<: *logging
image: afis-web:local
build:
context: .
dockerfile: docker/web.Dockerfile
# Nicht erst in einer Registry nachfragen: das Image entsteht hier.
# Ohne das versucht `up` einen Pull und warnt "pull access denied".
pull_policy: never
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
&& node_modules/.bin/tsx prisma/seed.ts
&& 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
# Links der Port auf dem Host, rechts der im Container. Nur der linke ist
# frei waehlbar - ueber WEB_PORT in der .env, damit fuer eine andere
# Portwahl keine verfolgte Datei angefasst werden muss.
ports:
- '${WEB_PORT:-3000}:3000'
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: afis-worker:local
build:
context: .
dockerfile: docker/worker.Dockerfile
# Nicht erst in einer Registry nachfragen: das Image entsteht hier.
# Ohne das versucht `up` einen Pull und warnt "pull access denied".
pull_policy: never
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
# Ausdruecklich 6432: neuere Fassungen des Images lauschen sonst auf
# 5432, und dann zeigen sowohl DATABASE_URL als auch der Healthcheck
# ins Leere. 6432 haelt ausserdem sichtbar auseinander, ob eine
# Verbindung ueber den Pool geht oder an ihm vorbei.
LISTEN_PORT: '6432'
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: afis-pdf:local
build:
context: .
dockerfile: docker/pdf.Dockerfile
# Nicht erst in einer Registry nachfragen: das Image entsteht hier.
# Ohne das versucht `up` einen Pull und warnt "pull access denied".
pull_policy: never
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
# Ein Weg von aussen, falls kein eigener Reverse Proxy vorhanden ist.
# Startet nur auf ausdrueckliche Anforderung:
# docker compose --profile cloudflare up -d
# Wer einen Proxy im eigenen Netz betreibt - Nginx Proxy Manager, Caddy,
# Traefik -, laesst diesen Dienst weg und leitet dort auf den Port aus
# `ports` beim Dienst `web`.
cloudflared:
<<: *logging
profiles: ['cloudflare']
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
# Auslagerungs-Backup. Braucht RESTIC_REPOSITORY und RESTIC_PASSWORD und
# beendet sich ohne beides sofort wieder - deshalb nur auf Anforderung:
# docker compose --profile offsite up -d
offsite-backup:
<<: *logging
profiles: ['offsite']
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: