`docker compose up` fragt zuerst in einer Registry nach, auch wenn das Image lokal gebaut wird, und warnt dann "pull access denied". Mit `pull_policy: never` nimmt Compose gleich das lokale Image. Betrifft vor allem den Start-Knopf in Dockge, der `up -d` ohne vorheriges `build` ausfuehrt.
226 lines
6.7 KiB
YAML
226 lines
6.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
|
|
#
|
|
# 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
|
|
# 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 && 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
|
|
# 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
|
|
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
|
|
|
|
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:
|