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
Die Adresse zeigte auf /var/run/clamav/clamd.sock in einem geteilten Volume. Das offizielle clamav-Image legt seinen Unix-Socket aber unter /tmp im eigenen Container an - nachgemessen, das Verzeichnis im Volume bleibt leer. Jeder Verbindungsversuch lief damit in ENOENT, und die Statusseite meldete die Virenpruefung dauerhaft als gestoert. Folgenreich war das nicht nur fuer die Anzeige: ohne erreichbaren Scanner bleiben Uploads auf "Pruefung unvollstaendig" liegen und brauchen die Freigabe von Hand. Die Kette hat also gehalten, aber niemand kam durch. Dasselbe Image oeffnet ab Werk TCPSocket 3310. Darueber laeuft die Anbindung jetzt; der Port ist im Stack nicht veroeffentlicht und nur im internen Netz erreichbar. CLAMAV_ADDRESS nimmt weiterhin einen Pfad an, falls irgendwo doch ein Unix-Socket erreichbar ist. Das Socket-Volume faellt weg, es enthielt nie etwas. Der Scanner sieht ausserdem die Dateiablage nicht mehr: uebertragen wird per INSTREAM durch die Verbindung, gelesen hat er dort nie etwas. Geprueft gegen ein laufendes clamd, einschliesslich EICAR. Diese Pruefung bleibt als abschaltbarer Teil der Suite stehen: CLAMAV_LIVE=clamav:3310 pnpm --filter @afis/web test pruefkette Die uebrigen neuen Pruefungen decken das Zerlegen der Adresse ab. Sie haetten den Fehler nicht gefunden - eine Adresse, die auf nichts zeigt, ist syntaktisch tadellos. Nur eine echte Verbindung beantwortet das.
253 lines
8.2 KiB
YAML
253 lines
8.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
|
|
# - 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
|
|
# 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'
|
|
# Nur die Signaturen. Der Scanner bekommt die Daten ueber INSTREAM durch
|
|
# die Verbindung gereicht und braucht die Dateiablage deshalb nicht zu
|
|
# sehen - siehe lib/files/clamav.ts. Ein Socket-Volume gibt es nicht mehr:
|
|
# das offizielle Image legt seinen Unix-Socket unter /tmp im eigenen
|
|
# Container an, das geteilte Verzeichnis blieb leer, und die Virenpruefung
|
|
# meldete sich dauerhaft als gestoert. Die Anbindung laeuft ueber
|
|
# TCPSocket 3310, den dasselbe Image ab Werk oeffnet. Der Port ist nicht
|
|
# veroeffentlicht und nur im Stack-Netz erreichbar.
|
|
volumes:
|
|
- clamav_db:/var/lib/clamav
|
|
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:
|