Files
AfiS-Aktensystem/docs/11-betrieb.md
T
HarlekinandClaude Opus 5 4446e3f0ac Fachdokumente nach docs/, Entscheidungen festgehalten
CLAUDE.md und README.md verweisen auf docs/, die Dateien lagen aber im
Wurzelverzeichnis. Verschoben.

docs/13-entscheidungen.md hält fest, wo die Dokumente sich widersprechen und
wie es aufgelöst wurde. Der wichtigste Punkt: 04-datenmodell.md verlangt eine
Regel, die jedes UPDATE auf CaseEntryVersion verwirft, 05-akten-und-vorlagen.md
verlangt, dass eine Umklassifizierung rückwirkend auf alle Versionen wirkt.
Beides zusammen geht nicht. Aufgelöst über einen Trigger, der die Tabelle
anhängend hält, aber das Anheben von minClearance zulässt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 00:00:58 +02:00

264 lines
8.2 KiB
Markdown

# Betrieb mit Dockge
Der Stack wird über [Dockge](https://github.com/louislam/dockge) verwaltet, nicht über die
Kommandozeile. Das bestimmt ein paar Entscheidungen, die sonst anders aussähen.
## Was daraus folgt
1. **Ein einziger Stack, eine `compose.yaml`.** Dockge verwaltet Stacks getrennt und legt für
jeden ein eigenes Netzwerk an. Mehrere Stacks müsste man von Hand zusammenstecken, also
kommt alles in einen.
2. **Keine Einmal-Container.** Dockge zeigt beendete Container nicht dauerhaft an. Ein
Migrations- oder Backup-Job als `restart: no` wäre unsichtbar, wenn er seit Wochen
fehlschlägt. Migrationen laufen deshalb im Startkommando von `web`, Backups in
Dauerläufern mit eigenem Cron.
3. **Healthchecks sind die Statusanzeige.** Dockge zeigt sie direkt in der Übersicht. Jeder
Dienst braucht einen, sonst siehst du nur „läuft" und weißt nichts.
4. **Named Volumes statt Bind Mounts.** Überstehen Stack-Updates zuverlässig und tauchen in
Dockge sauber auf. Ausnahme ist das Backup-Ziel, das gehört auf eine echte Platte.
5. **Kein Caddy.** Der Cloudflare Tunnel übernimmt TLS und Kompression, und Next.js liefert
seine statischen Dateien selbst mit korrekten Cache-Headern aus. Ein zusätzlicher Proxy
wäre nur ein weiterer Container, der kaputtgehen kann.
## Ablage
```
/opt/stacks/afis/
├── compose.yaml im Dockge-Editor bearbeitbar
└── .env eigener Reiter in Dockge
```
Beides wird über die Weboberfläche gepflegt. `.env` gehört nicht ins Git-Repository, die
Vorlage dafür ist `.env.example`.
## compose.yaml
```yaml
services:
web:
image: ghcr.io/DEIN-USER/afis-web:latest
restart: unless-stopped
command: sh -c "npx prisma migrate deploy && node server.js"
env_file: .env
environment:
NODE_ENV: production
PORT: "3000"
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:
image: ghcr.io/DEIN-USER/afis-worker:latest
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:
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:
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:
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:
image: ghcr.io/DEIN-USER/afis-pdf:latest # Playwright-Basis
restart: unless-stopped
environment:
PORT: "3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN}
depends_on:
web: { condition: service_healthy }
db-backup:
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 }
offsite-backup:
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
volumes:
pgdata:
files:
clamav_db:
clamav_socket:
```
`DEIN-USER` durch deinen GitHub-Namen ersetzen. Die Images baut ein GitHub-Actions-Workflow
und schiebt sie in die GitHub Container Registry. Dockge zieht sie beim Update.
## Erste Einrichtung
1. In Dockge einen neuen Stack `afis` anlegen
2. `compose.yaml` einfügen, `DEIN-USER` ersetzen
3. Im Reiter `.env` die Werte aus `.env.example` eintragen
4. Deploy. Beim ersten Start dauert `clamav` mehrere Minuten, bis die Signaturen da sind.
Solange bleibt der Container gelb, das ist normal.
5. Prüfen, dass `web` grün wird. Dann `https://afis.example.de` aufrufen.
## Aktualisieren
Dockge: Stack öffnen, „Update", fertig. Das zieht neue Images und startet neu.
Die Migration läuft automatisch im Startkommando von `web`.
**Vor jedem Update ein Dump.** Der läuft zwar nachts, aber ein Update mittags nach einer
kaputten Migration ohne frisches Backup ist genau der Fall, den man einmal erlebt.
In der Dockge-Konsole des `db-backup`-Containers:
```
/backup.sh
```
## Wiederherstellung
Aus der Dockge-Konsole des `db`-Containers:
```bash
pg_restore -U afis -d afis --clean --if-exists /backups/daily/afis-<datum>.sql.gz
```
Dateien liegen im Volume `files`. Aus dem restic-Repository:
```bash
restic restore latest --target /restore --include /data/files
```
**Einmal im Monat wirklich durchspielen**, in eine Wegwerf-Datenbank. Ein ungetestetes Backup
ist keins, und das ist der Punkt, an dem Homeserver-Projekte reihenweise scheitern.
## Was nicht nach außen darf
Nur `web` hat eine Portfreigabe, und die geht ins LAN. `db`, `pgbouncer`, `clamav` und `pdf`
sind ausschließlich im Stack-Netz erreichbar. Am Router wird kein Port geöffnet, der Tunnel
verbindet sich ausgehend.
Der Dockge-Zugang selbst (Port 5001) gehört ebenfalls nicht ins Internet. Wenn du von
unterwegs dran willst: über Tailscale oder einen zweiten, per Cloudflare Access geschützten
Tunnel, nie offen.
## Beobachtung
- Dockge-Übersicht als erste Anlaufstelle: alle Healthchecks auf einen Blick
- `/api/health` prüft Datenbank, Discord-Gateway und ClamAV und wird vom Healthcheck genutzt
- Der Worker meldet Start, Verbindungsabbruch und fehlgeschlagenen Voll-Abgleich in den
Admin-Discord-Kanal. Das ist deine eigentliche Alarmierung, weil du Discord ohnehin offen hast.
- Logs über die Dockge-Oberfläche. Zusätzlich im Compose begrenzen, sonst läuft die Platte voll:
```yaml
x-logging: &logging
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
```
Als Anker `<<: *logging` bei jedem Dienst einhängen.
## Aufräumen
Tägliche Aufgabe in der Anwendung, kein eigener Container: abgelaufene Sessions löschen,
Quarantäne-Dateien älter als 24 Stunden löschen, PDF-Cache älter als 30 Tage löschen.
Der Audit-Log wird nie gelöscht.