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>
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user