dorfteich/docs/vs-nfd/70-betriebshandbuch.md
Claude Fable 5 18239e2fa9
All checks were successful
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m15s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m19s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m13s
CI / Import/export fidelity gate (push) Successful in 54s
CI / Lint, typecheck, test (pull_request) Successful in 6m20s
CI / Build container images (pull_request) Successful in 1m12s
CI / Auth e2e pack (pull_request) Successful in 8m24s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 22s
#221: offline update path incl. migrations, rehearsed with rollback
Adds docs/operations/update-runbook.md (obtain, verify by digest, back
up, apply, verify, roll back) with the migration behaviour stated
explicitly: a failed migration rolls back its own transaction but is
recorded in _prisma_migrations and blocks every further migrate deploy
(P3009) — including a re-deployed old image — until migrate resolve
--rolled-back; semantically irreversible migrations have exactly one way
back, the pre-update backup set. No rolling updates on a compose stage.
Rehearsed in the isolated environment of #220: regular update to a v2
image set, then a deliberate failed-update (P3018 division by zero,
schema change proven rolled back) with image-rollback-alone shown
insufficient and the documented recovery executed. Protocol:
docs/vs-nfd/98-update-rollback-protokoll.md. ADR 0024 decisions 5+6
recorded as executed; operations handbook and restore runbook updated;
plan checkbox P1-3 ticked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
2026-07-31 17:26:50 +02:00

18 KiB
Raw Blame History

Betriebshandbuch (VS-NfD, Issue #229)

Zweck: der Betreiber führt die Instanz ohne uns — auch an dem Tag, an dem etwas ausfällt. Dieses Handbuch bündelt die betrieblichen Prozeduren und verweist für Schrittfolgen auf die maßgeblichen Runbooks im Repository, statt sie zu duplizieren. Das Kapitel mit dem größten VS-Gewicht ist §5 Löschung und Vernichtung; es baut auf der vollständigen Kopienliste der Sicherheitsdokumentation auf (60-sicherheitsdokumentation.md §5).

Belegstufen. Jede Prozedur nennt am Ende ihren Erprobungsstand:

  • erprobt — vom Autor bzw. automatisiert mindestens einmal real ausgeführt, mit Beleg.
  • ⚠️ Mechanik vorhanden, nicht geprobt — implementiert und getestet, aber noch nie im Ernstfall/als Übung durchgespielt.
  • offen — kommt mit dem genannten Issue.

1 Installation

Referenz: deploy/stages.md (maßgebliche Schrittfolge), docs/self-hosting/README.md.

  1. Host-Voraussetzungen: Docker + Compose, ein Reverse Proxy mit TLS (oder das mitgelieferte caddy-Compose-Profil).
  2. deploy/compose/docker-compose.yml + .env (Vorlage .env.example; niemals eine echte .env ins Repo — CI-Zaun #198) auf den Host bringen; COMPOSE_PROJECT_NAME, Ports (WEB_PORT/API_PORT/COLLAB_PORT), POSTGRES_PASSWORD, Secrets setzen.
  3. docker compose pull && docker compose up -d — die api wendet Migrationen beim Start selbst an (MIGRATE_ON_START); es gibt keinen separaten Migrationsschritt.
  4. Erststart: der Setup-Wizard (Issue #80) legt das Admin-Konto an und verriegelt sich danach dauerhaft; bis zum Abschluss ist nur /setup/* erreichbar.
  5. Reverse Proxy: / → web, /api → api, /collab → collab mit WebSocket-Upgrade (Portliste: Sicherheitsdokumentation §2).
  6. Für VS-NfD-Betrieb anschließend die Referenzkonfiguration aus dem Härtungsleitfaden (50-haertungsleitfaden.md) anwenden und mit dessen Checkliste verifizieren.

Belegstufe: erprobt — die Stages Test/Int/Prod auf ONE sind exakt nach dieser Prozedur aufgesetzt und laufen produktiv (deploy/stages.md dokumentiert die realen Instanzen).

Airgap-/Offline-Variante: teilweise verfügbar —

  • Mirror-Verfahren (#218): verfügbar und einmal end-to-end ausgeführt. Schrittfolge: Image-Liste generieren (deploy/scripts/list-images.sh — nie von Hand pflegen), digest-erhaltend kopieren (docker buildx imagetools create), Digest im Spiegel gegen den Compose-Pin verifizieren, Deployment per REGISTRY_PREFIX (Dritt-Images) bzw. IMAGE_PREFIX (eigene Images) auf den Spiegel zeigen. Vollständige Prozedur: deploy/stages.md §5b; Ausführungsnachweis: 95-mirror-protokoll.md.
  • Netzloser Build (#219): verifiziert und entschieden. Betrieb läuft aus Prebuilt-Images; zusätzlich ist der Workspace-Build ohne Netz zweifach reproduziert (Offline-Kit = pnpm-Store + Build-Cache; einzige Grenze: das installierbare drawio-Plugin-ZIP braucht sein vorab abgelegtes Vendor-Tarball). Entscheidung und Prozedur: ADR 0024 §Decisions, Nachweis: 96-offline-build-protokoll.md.
  • Isolationslauf (#220): live verifiziert. Vollständiges Deployment (Setup, Login, Live-Kollaboration, Suche, Upload, alle Exporte, Backup + Restore) in einer Umgebung ohne jeden Egress-Pfad (internal: true-Netze, tcpdump-Vollcapture: null Pakete nach außen). Einzige nach außen gerichtete Verbindung ist SMTP; ohne Egress bleibt die Instanz voll funktionsfähig, nur die Mail-Zustellung scheitert kontrolliert (Outbox, 5 Versuche, dann FAILED). Nachweis: 97-isolationslauf-protokoll.md.
  • Offline-Update-Pfad (#221): live verifiziert. Prozedur docs/operations/update-runbook.md; in der isolierten Umgebung geprobt inkl. eines absichtlichen Migrations-Fehlschlags mit Rollback. Nachweis: 98-update-rollback-protokoll.md. Grundlage weiterhin: alle Dritt-Images digest-gepinnt (#203), ein authoritativer Node-Pin (#236), SBOMs je Release (#202).

2 Update und Rollback

Referenz: docs/operations/update-runbook.md (maßgeblich, inkl. explizitem Migrationsverhalten und Rollback-Pfaden), docs/architecture/operations.md §Update strategy, deploy/stages.md.

  • Stages: Merge auf main → CD baut Images, deployt Test, führt Smoke-Tests aus, promotet Int. CD synct keine Compose-Dateien — Compose-Änderungen werden von Hand auf den Host übernommen, sonst räumt --remove-orphans manuell ergänzte Container ab. Belegstufe: erprobt (läuft bei jedem Merge; zuletzt CD-Lauf 568).
  • Produktion: ausschließlich getaggte Releases. git tag vX.Y.Z baut die Versions-Images und führt die Update-Simulation als Gate aus; git tag prod-vX.Y.Z-initial pinnt die Version in der Prod-.env und rollt aus (readyz-Poll). Migrationen laufen beim api-Start; Rückwärtsmigrationen gibt es nicht — Rollback setzt deshalb ein DB-kompatibles Vorgängerimage voraus (Migrations-Kaveat; im Zweifel Restore aus dem Backup-Set der Vorversion). Belegstufe: erprobt (alle Prod-Versionen bis v0.12.0 so ausgerollt).
  • Rollback: git tag prod-v<prev>-rollback1 pinnt die Vorversion zurück. Belegstufe: ⚠️ Mechanik vorhanden, nicht geprobt — ein realer Rollback war bisher nie nötig; die Übung steht aus.
  • Dritt-Image-Digest heben: Prozedur deploy/stages.md §5a (imagetools inspect → Compose-Referenz ändern → CI bestätigt → Stage- Composes von Hand nachziehen → docker inspect verifiziert). Belegstufe: erprobt (Rollout #203 am 31.07.2026).
  • Offline-Update: geprobt (#221) — Bundle-Beschaffung über das Mirror-Verfahren (§1), dann identische Prozedur; Fehlschlag-Rollback einmal durchgespielt (98-update-rollback-protokoll.md).

3 Backup und Restore

Referenz: docs/operations/restore-runbook.md (maßgeblich), deploy/backup-basel.md, ADR 0015/0026.

  • Nächtlich erzeugt der backup-Sidecar ein konsistentes Restore-Set (pg_dump -Fc + Tar der Volumes uploads/plugins) auf dem backups-Volume; Retention BACKUP_RETENTION_DAYS (Default 30, Test/Int 7). Status in status.json, Fehler alarmieren per Mail direkt via SMTP (bewusst nicht über die api — sie könnte das kaputte Teil sein).
  • Zielbeschränkung (#192, ADR 0026): Fernziele (rsync-Spiegel, WebDAV) funktionieren nur gegen Hosts der Deploy-Allowlist BACKUP_ALLOWED_TARGETS; eine leere Allowlist schaltet alle Fernziele hart ab. Die Allowlist ist bewusst NICHT über die Admin-UI änderbar (Rollentrennung, §6).
  • Restore: deploy/backup/restore.sh fährt den Stack kontrolliert herunter, spielt Dump + Volumes zurück und startet neu; Details und Totalverlust-Szenario im Runbook.
  • Geprobter Restore: erprobt — ein automatischer monatlicher Restore-Drill (drill-*-Tag → drill.yml) stellt das jüngste Prod-Set in einer Wegwerf-Umgebung wieder her und prüft Inhalte; das Protokoll jedes Laufs steht als Kommentar an Issue #98. Backups sind unverschlüsselt by design — Datenträgerschutz ist Plattformsache (Abgrenzungserklärung).

4 Wartungsjobs (Scheduler)

Alle Jobs laufen in der api (in-app Scheduler, jobs-Tabelle), sind im Site-Admin-Systempanel sichtbar und dort manuell auslösbar (auditiert als job.triggered). Aktueller Bestand (10):

Job Rhythmus Wirkung
trash-purge täglich endgültiges Löschen abgelaufener Papierkorb-Seiten und -Teiche (trash.retentionDays, #31/#193)
orphan-file-sweep täglich verwaiste Uploads entfernen (#194); trägt nachts auch den SHA-256-Backfill (#199)
page-compaction stündlich page_updates-Log in den Zustand mergen
version-thinning täglich automatische Versions-Snapshots ausdünnen
audit-retention täglich audit_log nach audit.retentionDays beschneiden (#196), Lücke selbst auditiert (audit.pruned)
conversion-payload-prune täglich Import-/Export-Payloads fertiger Jobs nullen (#233)
mail-outbox-retention täglich SENT/endgültig FAILED Outbox-Zeilen löschen (#234)
data-export-purge stündlich abgelaufene DSGVO-Datenexporte entfernen (#68)
notification-digest alle 15 min (Versand nach Nutzer-Präferenz) Benachrichtigungs-Digests versenden
read-trail-maintenance täglich Lesetrail-Partitionen vorausanlegen + read_events nach readTrail.retentionDays beschneiden (#224, auditiert read_trail.pruned)

Der e2e-Zaun apps/web/e2e/system.spec.ts pinnt diese Zahl — ein neuer Job ohne Handbuch-/Zaun-Anpassung wird rot.

5 Löschung und Vernichtung

Grundsatz: „Gelöscht" heißt in Dorfteich erst dann gelöscht, wenn alle Kopien aus 60-sicherheitsdokumentation.md §5 erreicht sind. Die Tabelle nennt je Inhaltstyp, was die Löschung tut, welche Kopien sie erreicht und was stehen bleibt.

Vorgang Wirkung erreichte Kopien Rückstände / Fristen
Seite in den Papierkorb Soft-Delete (deleted_at), aus Suchindex entfernt (#195) Suchvektor sofort Inhalt vollständig vorhanden, wiederherstellbar bis Purge
Seiten-Purge (automatisch nach trash.retentionDays, Default 30, oder manuell) löscht Zustand, Update-Log, Content-Cache, Versionen, Kommentare, Attachments (Bytes + Quota), Watches; Kinder rücken auf DB-Zeilen + uploads-Bytes page_links.target_slug bleibt bewusst (#235, Restrisiko I-24); Backups/Spiegel bis Ablauf ihrer Retention; Export-/Endgeräte-Kopien organisatorisch
Teich-Purge (#193, automatisch/manuell, Site-Admin) löscht alle Seiten samt Kaskade, Labels, Grants, Nutzungszähler, Plugin-Opt-ins, Conversion-Jobs, Files wie oben, teichweit wie oben
Nutzer löschen / DSGVO Konto löschen bzw. Autorschaft pseudonymisieren (user.pseudonymized) Identitätsdaten von ihm erstellte Inhalte gehören dem Teich
Export-Payloads conversion-payload-prune nullt input/result fertiger Jobs (Default 30 d) DB Job-Zeile bleibt für Status/Audit
Mail-Kopien mail-outbox-retention löscht SENT/endgültig FAILED (Default 30 d) DB zugestellte Mails beim Empfänger (Restrisiko I-23)
Backup-Sets Retention des Sidecars bzw. des Spiegels backups-Volume, Spiegel ein gelöschter Inhalt lebt maximal bis zum Ablauf der längsten Backup-Retention weiter — bei sofortigem Vernichtungsbedarf Sets manuell löschen (Host-Zugriff) und Spiegel bereinigen
Audit-Trail audit-retention (Default 365 d) DB Metadaten, nie Inhalt

Sofortige Vernichtung einzelner Inhalte (über die Fristen hinaus): Seite manuell purgen (Papierkorb → endgültig löschen), danach auf dem Host die Backup-Sets der Aufbewahrungskette löschen bzw. den Spiegel bereinigen und Endgeräte-/Exportkopien organisatorisch einsammeln. Belegstufe Purge-Pfade: erprobt (laufen täglich produktiv; Purge- Semantik durch Tests gepinnt). Belegstufe „Backup-Kette manuell vernichten": ⚠️ nicht geprobt.

Außerbetriebnahme einer Instanz: docker compose down -v entfernt Container und alle benannten Volumes (db-data, uploads, plugins, secrets, backups, Caddy-Volumes); anschließend Spiegel- Bestände löschen und Host-Datenträger nach Betreiber-Vorgabe vernichten — die physische Vernichtung ist Plattformsache (Abgrenzungserklärung). Belegstufe: erprobt für den Stack-Teil (der stillgelegte Alt-VPS wurde so zurückgebaut; Datenträger dort noch als Rollback-Reserve vorhanden).

6 Rollentrennung

Aufgabe braucht
Instanz-Settings, Nutzer-/Quota-Verwaltung, Plugins, Wartungsjobs anstoßen, Teich-Purge, Audit-Panel Site-Admin (App-Rolle)
Deploy, .env/Compose ändern, Backup-Allowlist, Secrets-Volume, Restore, Backup-Sets vernichten, DB-Direktzugriff, Log-Collector Plattformzugriff (Host)
Release nach Prod Plattformzugriff + Freigabeprozess (Tag)

Was ein Site-Admin nicht kann (bewusste Grenzen):

  • die Backup-Zielliste ändern oder Fernziele aktivieren (BACKUP_ALLOWED_TARGETS ist Deploy-Konfiguration, ADR 0026);
  • Secrets lesen oder setzen, die im Secret-Store/der .env liegen (die Settings-Tabelle trägt nie Secrets, security.md §Secrets);
  • Deploy-/Composeänderungen, Image-Versionen, Migrationen auslösen;
  • den Audit-Trail editieren (nur lesen; Retention läuft als Job und protokolliert sich selbst);
  • Backups herunterladen ist möglich (In-App-Restore-Pfad, #103), aber ihre Vernichtung auf Host/Spiegel nicht.

Zu beachten: ein Site-Admin liest per Rollen-Bypass jeden Inhalt der Instanz (permissions.md) — die Trennung „wer darf Inhalte sehen" von „wer betreibt die Plattform" ist damit App-seitig bewusst NICHT absolut; wo das nicht tragbar ist, ist es organisatorisch zu regeln (Vier-Augen-Prinzip bei Vergabe der Site-Admin-Rolle). Der Lesetrail für eingestufte Inhalte (M29, #222#225) macht solche Zugriffe nachvollziehbar.

7 Monitoring und Störung

Referenz: deploy/monitoring.md, docs/architecture/operations.md §Health & monitoring.

  • GET /api/v1/readyz prüft DB, Migrationen, Konverter, Renderer, Backup-Status; healthz je Container. Externe Überwachung der Referenzinstanzen: Uptime-Kuma mit Alarmierung.
  • Logs: alle Dienste loggen JSON auf stdout (Docker json-file mit Rotation); Audit-Ereignisse mit msg-Präfix audit: und severity-Feld — Weiterleitung an SIEM/Syslog übernimmt der Collector des Betreibers (Katalog: docs/architecture/audit-events.md, v1.1, mit Kompatibilitätsversprechen).
  • Integritätsalarm: file.integrity_failed (critical) = Download-Hash ≠ Upload-Hash → Objekt als manipuliert/korrupt behandeln, Datei aus Backup-Set wiederherstellen (Runbook), erneut laden; der Audit-Eintrag trägt beide Hashes.