Das Design ist in Abschnitt 8 von docs/architecture/23-ops.md beschrieben. Dies ist das Runbook für das Docker-Compose-Produkt. Es ist so geschrieben, dass eine Person es befolgen kann, die nicht an seiner Erstellung beteiligt war. Ist ein Schritt unklar, ist das ein Fehler in diesem Dokument.
Was geschützt wird und wie
Element
Vorgehen
Speicherort
Datenbank
WAL wird vom ersten Start an laufend archiviert, höchstens alle 60 Sekunden
Volume pgwal
Datenbank
Basis-Sicherungen mit pg_basebackup, standardmäßig täglich (backup-scheduler)
Volume pgbackup
Datenbank
Verschlüsselte Kopien der Basis-Sicherungen und des WAL alle fünf Minuten (backup-offsite)
Ein von Ihnen angegebener separater Speicher
Dateien
Volume files. Kopieren Sie es mit dem Sicherungswerkzeug Ihres Hosts oder verwenden Sie versionierten Objektspeicher
Volume files
Secrets
docker/.env, insbesondere QUIRE_MASTER_KEY (und jedes noch verwendete QUIRE_MASTER_KEY_RETIRED), QUIRE_BACKUP_ENCRYPTION_KEY sowie docker/secrets/audit-signing-key.pem
Bewahren Sie eine Kopie außerhalb dieses Hosts auf
Suchindizes, Caches, abgeleitete Dateien
Werden nicht gesichert, sondern neu erstellt
Ziele: ein Wiederherstellungspunkt höchstens 60 Sekunden vor dem Ausfall und eine Wiederherstellung innerhalb von 60 Minuten bei einer 500-GB-Datenbank.
Zwei Fehler passieren häufig. Eine Datenbank, die ohne ihre Dateien wiederhergestellt wird, zeigt defekte Seiten. Eine Datenbank, die ohne QUIRE_MASTER_KEY wiederhergestellt wird, kann enthaltene Zugangsdaten für SSO, Webhooks und Integrationen nicht entschlüsseln. Bis eine Master-Schlüssel-Rotation ohne ungelöste Werte abgeschlossen ist (key-rotation.md), gilt das auch für ausgemusterte Schlüssel. Beides gehört zur Sicherung.
Sicherungen erstellen
Eine Basissicherung des gesamten Clusters:
docker compose -f docker/compose.yaml --profile backup run --rm backup
Der Vorgang behält die neuesten QUIRE_BACKUP_KEEP Basissicherungen (standardmäßig 5) und löscht WAL, das die älteste davon nicht mehr benötigt, damit das Archiv unbegrenzt wachsen kann. Richten Sie auf dem Host einen täglichen Cronjob oder systemd-Timer ein:
15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1
Oder überlassen Sie die Planung dem Stack: Das Profil backup startet backup-scheduler, das alle QUIRE_BACKUP_INTERVAL_HOURS (standardmäßig 24) eine Basissicherung erstellt, sowie den unten beschriebenen Dienst backup-offsite.
docker compose -f docker/compose.yaml --profile backup up -d
Verschlüsselte externe Kopien
Beide Volumes liegen auf demselben Host wie die Datenbank; eine Sicherung auf dem ausgefallenen Rechner ist keine Sicherung. backup-offsite kopiert jede Basissicherung und jedes archivierte WAL-Segment über den Speicheranschluss verschlüsselt in einen separaten Speicher und bewahrt sie dort entsprechend der Aufbewahrungsfrist auf:
Verschlüsselung. AES-256-GCM mit QUIRE_BACKUP_ENCRYPTION_KEY (oder der in QUIRE_BACKUP_ENCRYPTION_KEY_FILE angegebenen Datei): 32 Byte, erzeugt mit openssl rand -hex 32. Jede Datei hat eine eigene Nonce und ein Authentifizierungs-Tag. Eine Kopie ist daher ohne Schlüssel unlesbar; jede Veränderung wird erkannt. Bewahren Sie den Schlüssel zusammen mit QUIRE_MASTER_KEY, aber getrennt von diesem Host und vom Sicherungsspeicher auf. Ohne diesen Schlüssel ist keine Wiederherstellung möglich.
Speicherort.QUIRE_BACKUP_STORAGE_DRIVER ist s3, azure oder local (ein eingehängtes externes Laufwerk unter QUIRE_BACKUP_STORAGE_ROOT). Die Einstellungen entsprechen denen des Dateispeichers und tragen den Präfix QUIRE_BACKUP_: QUIRE_BACKUP_S3_ENDPOINT, QUIRE_BACKUP_S3_BUCKET, QUIRE_BACKUP_S3_ACCESS_KEY_ID und so weiter. Verwenden Sie einen anderen Bucket und idealerweise ein anderes Konto als für Dateien sowie Zugangsdaten, die Schreiben, aber nach Möglichkeit des Anbieters kein Löschen erlauben.
Aufbewahrung. Die neuesten QUIRE_BACKUP_OFFSITE_KEEP Basissicherungen (standardmäßig QUIRE_BACKUP_KEEP, andernfalls 7) und das von der ältesten benötigte WAL. Ältere Sicherungssätze und Segmente werden aus dem Speicher gelöscht.
Zeitpunkt. Alle QUIRE_BACKUP_SHIP_INTERVAL_SECONDS (standardmäßig 300). Der Versand ist idempotent: Bereits gespeicherte Inhalte werden übersprungen. Eine Basissicherung gilt erst dann als gespeichert, wenn ihr Manifest zuletzt geschrieben wurde.
Derselbe Vorgang kann manuell ausgeführt werden:
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts shipdocker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts verify
Um auf einem neuen Host wiederherzustellen, holen Sie zunächst einen Sicherungssatz zurück und führen Sie dann die nachfolgenden Schritte aus. Verwenden Sie dabei das abgerufene Verzeichnis anstelle des Volumes pgbackup und das abgerufene wal-archive anstelle von pgwal:
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore
Wiederherstellung zu einem bestimmten Zeitpunkt
Verwenden Sie dies nach Datenverlust: einem fehlerhaften Import, einem gelöschten Kurs oder einer Vertragsmigration, die rückgängig gemacht werden muss. Die aktive Datenbank wird ersetzt. Üben Sie daher zuerst die nachfolgend beschriebene Wiederherstellung.
Wählen Sie den Zielzeitpunkt in UTC, kurz vor dem Schaden: 2026-09-24 09:30:00+00. Im Audit-Protokoll (/admin/audit) ist der Zeitpunkt üblicherweise zu sehen.
Beenden Sie alle schreibenden Dienste: docker compose -f docker/compose.yaml stop web content worker scheduler collab
Bewahren Sie den beschädigten Cluster auf, bis die Wiederherstellung geprüft wurde:
docker compose -f docker/compose.yaml stop postgresdocker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/
Entpacken Sie die neueste Basissicherung vor dem Zielzeitpunkt in das Daten-Volume und richten Sie eine gezielte Wiederherstellung ein:
docker run --rm -v quire_pgbackup:/backups:ro -v quire_postgres18-data:/var/lib/postgresql postgres:18-alpine sh -euc ' base="$(ls -1d /backups/base-* | sort | tail -n 1)" # or the one before the target rm -rf /var/lib/postgresql/18/docker && mkdir -p /var/lib/postgresql/18/docker tar -xzf "$base/base.tar.gz" -C /var/lib/postgresql/18/docker touch /var/lib/postgresql/18/docker/recovery.signal chown -R postgres:postgres /var/lib/postgresql/18/docker && chmod 700 /var/lib/postgresql/18/docker'
Führen Sie die Wiederherstellung durch: Starten Sie Postgres einmal mit den Wiederherstellungseinstellungen über eine Compose-Override-Datei, damit die reguläre Datei unverändert bleibt:
Die Override-Datei ersetzt den gesamten Befehl und wiederholt deshalb die beiden benötigten Einstellungen: max_connections darf nicht kleiner als der Wert des Primärservers sein (sonst bricht die Wiederherstellung mit „insufficient parameter settings“ ab) und die eingehängte pg_hba.conf.
docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgresdocker compose -f docker/compose.yaml logs -f postgres # wait for "database system is ready"
Prüfen Sie die Datenbank, bevor Sie wieder Zugriff ermöglichen: Prüfen Sie die Auditkette (docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts) und ob die verlorenen Daten zurück sind.
Kehren Sie zum Normalbetrieb zurück: docker compose -f docker/compose.yaml up -d. Dadurch wird Postgres mit aktivierter Archivierung neu gestartet und eine neue WAL-Zeitlinie begonnen. Erstellen Sie sofort eine neue Basissicherung.
Der Projektname quire wird jedem Volume vorangestellt; docker volume ls zeigt die genauen Namen.
Geplante Verifizierungsübung
backup-offsite führt außerdem alle QUIRE_BACKUP_DRILL_INTERVAL_HOURS (standardmäßig 168, also wöchentlich) eine Übung aus. Nach einem Fehlschlag wird sie beim nächsten Durchlauf erneut ausgeführt. Sie ruft die neueste externe Basissicherung sowie alle danach archivierten WAL-Segmente ab, entschlüsselt sie (damit wird geprüft, dass der Schlüssel sie noch öffnen kann und nichts verändert wurde), vergleicht jede Datei mit ihrem Manifest, prüft, ob das Archiv ein Postgres-Datenverzeichnis ist, und vergewissert sich, dass das WAL seit der Sicherung lückenlos ist. Der Bericht wird als reports/drill-<time>.json im Speicher und im Protokoll des Diensts abgelegt. Bei einem Fehlschlag wird die Datei oder das erste fehlende Segment genannt.
Die Wiederherstellungsübung
Eine Sicherung, die nie wiederhergestellt wurde, ist keine Sicherung. Die Übung stellt die neueste vollständige Basissicherung vor dem Zielzeitpunkt samt WAL-Archiv in einer isolierten Test-Postgres-Instanz wieder her und prüft das Ergebnis:
docker/scripts/restore-drill.sh # to ninety minutes agodocker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"
Der Zielzeitpunkt muss UTC und exakt in diesem Format angegeben sein. Es muss eine ältere Basissicherung und archiviertes WAL nach dem Zielzeitpunkt geben. Erstellen Sie bei einer neuen Installation eine Basissicherung und warten Sie das nächste archivierte Segment ab (bei Schreibvorgängen höchstens eine Minute), bevor Sie einen späteren Zielzeitpunkt wählen. Die Übung benötigt auf dem Host nur Docker und bash.
Jeder der folgenden Schritte kann die Übung fehlschlagen lassen:
Wiederherstellbarkeit: Der Test-Cluster spielt die Protokolle bis zum Zielzeitpunkt ein und lässt sich öffnen.
Vollständigkeit: Die Zeilenanzahl jeder Tabelle wird mit der aktiven Datenbank verglichen (tooling/restore-drill). Die aktive Datenbank hat sich seit dem Zielzeitpunkt weiterentwickelt. Eine Tabelle darf daher in beide Richtungen um den größeren Wert aus 500 Zeilen und einem Zehntel ihrer Größe abweichen (Schreibvorgänge lassen sie zurückfallen, Löschungen führen zu mehr wiederhergestellten Zeilen). Eine Partition, die erst nach dem Zielzeitpunkt erstellt wurde, gilt nicht als verlorene Tabelle. Erhöhen Sie bei stärker ausgelasteten Installationen die Toleranz mit QUIRE_DRILL_MAX_BEHIND und QUIRE_DRILL_MAX_DRIFT_RATIO. Eine fehlende oder geleerte Tabelle lässt die Prüfung fehlschlagen.
Integrität: Die Audit-Hash-Kette wird in der wiederhergestellten Kopie verifiziert.
Nutzbarkeit: Die Anwendungsrolle liest Daten über Row-Level-Security.
Dauer: Zeit vom Start bis zum Erfolg, verglichen mit QUIRE_DRILL_RTO_SECONDS (standardmäßig 3600).
Die Übung schreibt niemals in die aktive Datenbank oder ihre Volumes: Sicherungs- und WAL-Volumes sind schreibgeschützt eingehängt und der Test-Cluster wird am Ende – unabhängig vom Ergebnis – entfernt.
Setzen Sie QUIRE_DRILL_REPORT auf einen Pfad, damit in jedem Fall ein JSON-Bericht geschrieben wird. Richten Sie die Übung auf dem Docker-Host nach einem Zeitplan ein:
Führen Sie sie monatlich und vor jedem Upgrade durch. Ein fehlgeschlagener Test verhindert das Upgrade. Lassen Sie einmal pro Quartal eine Person, die dieses Runbook nicht geschrieben hat, auf einem Reserve-Host anhand ausschließlich dieses Dokuments eine echte Point-in-Time-Wiederherstellung durchführen.
Dateien
Lokale Dateien liegen im Volume files. Sichern Sie es gleichzeitig mit der Datenbank und stellen Sie beides gemeinsam wieder her:
docker run --rm -v quire_files:/files:ro -v "$PWD":/out alpine tar -czf /out/files-$(date -u +%Y%m%d).tar.gz -C /files .
Aktivieren Sie bei Objektspeicher die Bucket-Versionierung und bewahren Sie nicht aktuelle Versionen 35 Tage lang auf. Die Point-in-Time-Wiederherstellung von Dateien wird dann durch den Bucket selbst ermöglicht.