---
title: "Sicherung, Point-in-Time-Wiederherstellung und Wiederherstellungsübung"
description: "Sichern Sie Quire, stellen Sie es auf einen früheren Zeitpunkt zurück und weisen Sie die Wiederherstellung mit einer Übung nach."
image: "https://docs.quirelms.com/og.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.quirelms.com/de/llms.txt
> Use this file to discover all available pages before exploring further.

# Sicherung, Point-in-Time-Wiederherstellung und Wiederherstellungsübung

<span id="backup-point-in-time-recovery-and-the-restore-drill"></span>

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 <!--quire:what-is-protected-and-how-->

| 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](/de/ops/key-rotation/)), gilt das auch für ausgemusterte Schlüssel. Beides gehört zur Sicherung.

## Sicherungen erstellen <!--quire:taking-backups-->

Eine Basissicherung des gesamten Clusters:

```sh
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:

```cron
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`.

```sh
docker compose -f docker/compose.yaml --profile backup up -d
```

## Verschlüsselte externe Kopien <!--quire:encrypted-off-host-copies-->

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:

```sh
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts ship
docker 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`:

```sh
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore
```

## Wiederherstellung zu einem bestimmten Zeitpunkt <!--quire:restoring-to-a-point-in-time-->

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.

1. **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.
2. **Beenden Sie alle schreibenden Dienste**: `docker compose -f docker/compose.yaml stop web content worker scheduler collab`
3. **Bewahren Sie den beschädigten Cluster auf**, bis die Wiederherstellung geprüft wurde:
   ```sh
   docker compose -f docker/compose.yaml stop postgres
   docker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/
   ```
4. **Entpacken Sie** die neueste Basissicherung vor dem Zielzeitpunkt in das Daten-Volume und richten Sie eine gezielte Wiederherstellung ein:
   ```sh
   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'
   ```
5. **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:
   ```yaml
   # docker/compose.recover.yaml
   services:
     postgres:
       command: [postgres, -c, "restore_command=cp /var/lib/postgresql/wal-archive/%f %p",
                 -c, "recovery_target_time=2026-09-24 09:30:00+00",
                 -c, recovery_target_action=promote, -c, archive_mode=off,
                 -c, max_connections=200, -c, hba_file=/etc/postgresql/pg_hba.conf]
   ```
   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`.
   ```sh
   docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgres
   docker compose -f docker/compose.yaml logs -f postgres   # wait for "database system is ready"
   ```
6. **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.
7. **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 <!--quire:the-scheduled-verification-drill-->

`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 <!--quire:the-restore-drill-->

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:

```sh
docker/scripts/restore-drill.sh                                  # to ninety minutes ago
docker/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:

1. **Wiederherstellbarkeit**: Der Test-Cluster spielt die Protokolle bis zum Zielzeitpunkt ein und lässt sich öffnen.
2. **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.
3. **Integrität**: Die Audit-Hash-Kette wird in der wiederhergestellten Kopie verifiziert.
4. **Nutzbarkeit**: Die Anwendungsrolle liest Daten über Row-Level-Security.
5. **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:

```cron
30 3 1 * * cd /srv/quire && QUIRE_DRILL_REPORT=/var/log/quire-drill.json docker/scripts/restore-drill.sh >> /var/log/quire-drill.log 2>&1
```

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 <!--quire:files-->

Lokale Dateien liegen im Volume `files`. Sichern Sie es gleichzeitig mit der Datenbank und stellen Sie beides gemeinsam wieder her:

```sh
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.

Source: https://docs.quirelms.com/de/ops/backup-restore/index.mdx
