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>
264 lines
8.2 KiB
Markdown
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.
|