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
Der Tunnel war fest im Stack und damit Voraussetzung, obwohl er nur eine von mehreren Moeglichkeiten ist, die Anwendung von aussen erreichbar zu machen. Wer einen eigenen Reverse Proxy betreibt, brauchte ihn nie - und wollte womoeglich gerade nicht von einem fremden Dienst abhaengen. Er steht jetzt in einem Profil und startet nur mit `docker compose --profile cloudflare up -d`. Ohne Angabe bleibt er aus, und der Stack laeuft mit acht Diensten.
233 lines
7.0 KiB
YAML
233 lines
7.0 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 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. 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
|
|
|
|
# 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
|
|
|
|
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:
|