Airgapped sites pull from their own registry (ADR 0024). The image list
is GENERATED (deploy/scripts/list-images.sh resolves the compose file
incl. the caddy profile) so a mirror can never silently miss a service;
third-party images gain a configurable ${REGISTRY_PREFIX:-} in the
compose file (digest pins unchanged - Docker verifies the same sha256
regardless of which registry serves it), own images keep IMAGE_PREFIX;
no image reference is ever edited per site.
Step-by-step procedure in deploy/stages.md 5b: generate list, copy
digest-preservingly (docker buildx imagetools create; plain
pull/tag/push as the documented fallback - the digest comparison closes
the loop either way), verify the digest in the mirror against the pin,
point the deployment via REGISTRY_PREFIX/IMAGE_PREFIX.
Executed once end-to-end and recorded as assessor-facing evidence
(docs/vs-nfd/95-mirror-protokoll.md): all four third-party images
mirrored digest-identically into a local registry:2, plus
dorfteich-api:v0.12.0 (sha256:576f1646... identical on both sides; the
imagetools stall against the Gitea registry is recorded with its
workaround). Operations manual's airgap section now lists the mirror
part as available.
Refs #218.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
16 KiB
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.
- Host-Voraussetzungen: Docker + Compose, ein Reverse Proxy mit TLS
(oder das mitgelieferte
caddy-Compose-Profil). deploy/compose/docker-compose.yml+.env(Vorlage.env.example; niemals eine echte.envins Repo — CI-Zaun #198) auf den Host bringen;COMPOSE_PROJECT_NAME, Ports (WEB_PORT/API_PORT/COLLAB_PORT),POSTGRES_PASSWORD, Secrets setzen.docker compose pull && docker compose up -d— die api wendet Migrationen beim Start selbst an (MIGRATE_ON_START); es gibt keinen separaten Migrationsschritt.- Erststart: der Setup-Wizard (Issue #80) legt das Admin-Konto an und
verriegelt sich danach dauerhaft; bis zum Abschluss ist nur
/setup/*erreichbar. - Reverse Proxy:
/→ web,/api→ api,/collab→ collab mit WebSocket-Upgrade (Portliste: Sicherheitsdokumentation §2). - 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 perREGISTRY_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. - ⏳ offen: netzloser Build (#219), Testlauf in isolierter Umgebung (#220), Offline-Update-Pfad (#221); Meilenstein M28. Bereits vorhanden als Grundlage: alle Dritt-Images digest-gepinnt (#203), ein authoritativer Node-Pin (#236), SBOMs je Release (#202).
2 Update und Rollback
Referenz: 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-orphansmanuell ergänzte Container ab. Belegstufe: ✅ erprobt (läuft bei jedem Merge; zuletzt CD-Lauf 568). - Produktion: ausschließlich getaggte Releases.
git tag vX.Y.Zbaut die Versions-Images und führt die Update-Simulation als Gate aus;git tag prod-vX.Y.Z-initialpinnt die Version in der Prod-.envund 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>-rollback1pinnt 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 inspectverifiziert). Belegstufe: ✅ erprobt (Rollout #203 am 31.07.2026). - Offline-Update: ⏳ offen (#221, M28).
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 Volumesuploads/plugins) auf dembackups-Volume; RetentionBACKUP_RETENTION_DAYS(Default 30, Test/Int 7). Status instatus.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.shfä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 (9):
| 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 |
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_TARGETSist Deploy-Konfiguration, ADR 0026); - Secrets lesen oder setzen, die im Secret-Store/der
.envliegen (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/readyzprüft DB, Migrationen, Konverter, Renderer, Backup-Status;healthzje 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äfixaudit:undseverity-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.