Files
AfiS-Aktensystem/compose.yaml
T
Harlekin 8b0d2d5345
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
Images im Stack bauen statt aus einer Registry ziehen
Die drei eigenen Images standen als ghcr.io-Adressen mit einem Platzhalter im
Namen. Gebaut haetten sie nur GitHub Actions, und das Repository liegt nicht
auf GitHub - die Images gab es also nirgends, und wer den Platzhalter uebersah,
bekam "repository name must be lowercase".

Jetzt stehen die Bau-Anweisungen in der compose.yaml selbst. Das ist auch fuer
Dockge das richtige: dessen Editor zeigt nur diese eine Datei, eine
compose.override.yml waere unsichtbar gewesen und haette spaeter niemand mehr
erklaeren koennen.

Dazu ein Hinweis im Kopf der Datei, dass PORT und Healthchecks den Port *im*
Container meinen. Wer die Anwendung auf dem Host anders erreichen will, aendert
nur die linke Zahl bei ports.
2026-09-04 09:31:03 +02:00

217 lines
6.2 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
#
# 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. Wer die Anwendung auf dem Host anders erreichen will,
# aendert nur die linke Zahl bei `ports`, etwa "3010:3000".
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
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: afis-worker:local
build:
context: .
dockerfile: docker/worker.Dockerfile
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: afis-pdf:local
build:
context: .
dockerfile: docker/pdf.Dockerfile
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: