---
title: "Reservekopy, herstel op in tiidstip en de restore drill"
description: "Meitsje reservekopyen fan Quire, set it werom nei in tiidstip en bewize dat mei de restore drill."
image: "https://docs.quirelms.com/og.png"
---

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

# Reservekopy, herstel op in tiidstip en de restore drill

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

It ûntwerp stiet yn `docs/architecture/23-ops.md` seksje 8. Dit is de runbook foar it
Docker Compose-produkt. De hantlieding is skreaun om folge te wurden troch ien dy't him
net skreaun hat; as in stap ûndúdlik is, is dat in mankemint yn dit dokumint.

## Wat beskerme wurdt en hoe <!--quire:what-is-protected-and-how-->

| Asset | Hoe | Wêr |
| --- | --- | --- |
| De database | WAL wurdt hieltyd argivearre, maksimaal elke 60 sekonden, fan de earste boot ôf | `pgwal`-volume |
| De database | Base backups mei `pg_basebackup`, standert deistich (`backup-scheduler`) | `pgbackup`-volume |
| De database | Fersifere kopyen fan base backups en WAL, elke fiif minuten (`backup-offsite`) | In aparte store dy'tst sels neamst |
| Bestannen | It `files`-volume. Kopiearje mei it reservekopy-ark fan dyn host of brûk ferzjonearre objektopslach | `files`-volume |
| Geheimen | `docker/.env`, benammen `QUIRE_MASTER_KEY` (en elke `QUIRE_MASTER_KEY_RETIRED` dy't noch brûkt wurdt), `QUIRE_BACKUP_ENCRYPTION_KEY` en `docker/secrets/audit-signing-key.pem` | Bewarje in kopy bûten dizze host |
| Sykyndeksen, caches en renditions | Gjin reservekopy; wurde opnij makke | |

Doelen: in herstelpunt binnen 60 sekonden fan de fal ôf en in restore binnen 60 minuten foar
in database fan 500 GB.

Twa flaters komme faak foar. In database dy't weromset is **sûnder de bestannen**, jout brutsen
siden. In database dy't weromset is **sûnder `QUIRE_MASTER_KEY`**, kin de SSO-, webhook- en
yntegraasjebewizen yn de database net ûntsiferje; oant in master-keyrotaasje klear is sûnder
net-oploste wearden ([key-rotation.md](/fy/ops/key-rotation/)), jildt dit ek foar eardere kaaien.
Beide hearre by de reservekopy.

## Reservekopyen meitsje <!--quire:taking-backups-->

In base backup fan it hiele cluster:

```sh
docker compose -f docker/compose.yaml --profile backup run --rm backup
```

It behâldt de nijste `QUIRE_BACKUP_KEEP` base backups (standert 5) en snoeit WAL dy't de
âldste net mear nedich hat, sadat it argyf net ûnbeheind groeie kin. Plan it deistich mei
cron of in systemd-timer op de host:

```cron
15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1
```

Of lit de stack it planne: it `backup`-profile draait `backup-scheduler`, dat elke
`QUIRE_BACKUP_INTERVAL_HOURS` oeren in base backup makket (standert 24), en ek
`backup-offsite`, hjirnei beskreaun.

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

## Fersifere kopyen bûten de host <!--quire:encrypted-off-host-copies-->

Beide volumes steane op deselde host as de database; in reservekopy op in mislearre masine
is gjin reservekopy. `backup-offsite` kopiearret elke base backup en elk argivearre WAL-segmint
fia de storage-port fersifere nei in aparte store en hâldt se dêr neffens de bewaartermyn:

- **Fersifering.** AES-256-GCM mei `QUIRE_BACKUP_ENCRYPTION_KEY` (of it bestân neamd troch
  `QUIRE_BACKUP_ENCRYPTION_KEY_FILE`): 32 byte fan `openssl rand -hex 32`. Elk bestân hat
  in eigen nonce en autentikaasjetag; sûnder kaai is de kopy net te lêzen en elke feroaring
  wurdt ûntdutsen. Bewarje de kaai tegearre mei `QUIRE_MASTER_KEY`, fier fan dizze host en
  de reservekopy-store. Sûnder de kaai is der gjin restore.
- **Wêr.** `QUIRE_BACKUP_STORAGE_DRIVER` is `s3`, `azure` of `local` (in mounted remote disk
  op `QUIRE_BACKUP_STORAGE_ROOT`). Dit binne ynstellingen fan bestânsopslach mei prefix
  `QUIRE_BACKUP_`: `QUIRE_BACKUP_S3_ENDPOINT`, `QUIRE_BACKUP_S3_BUCKET`,
  `QUIRE_BACKUP_S3_ACCESS_KEY_ID` ensafuorthinne. Brûk in oare bucket en leafst in oar
  akkount as foar de bestannen, mei bewiis dat skriuwe mar net wiskje kin as de provider dat tastiet.
- **Bewartermyn.** De nijste `QUIRE_BACKUP_OFFSITE_KEEP` base backups (standert
  `QUIRE_BACKUP_KEEP`, oars 7) en de WAL dy't de âldste dêrfan nedich hat; âldere sets
  en segminten wurde út de store wiske.
- **Wannear.** Elke `QUIRE_BACKUP_SHIP_INTERVAL_SECONDS` (standert 300). It ferstjoeren
  is idempotint: al opsleine gegevens wurde oerslein, en in base backup telt pas as opslein
  neidat syn manifest as lêste skreaun is.

Deselde kommando's kinne mei de hân rinne:

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

Om op in nije host te herstellen, helje earst in set werom; folgje dan de stappen hjirûnder
mei de ophelle map ynstee fan it `pgbackup`-volume en it ophelle `wal-archive` ynstee fan `pgwal`:

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

## Herstellen nei in tiidstip <!--quire:restoring-to-a-point-in-time-->

Brûk dit nei gegevensferlies: in ferkearde ymport, in wiske kursus of in contractmigration
dy't weromdraaid wurde moat. It ferfangt de live database; oefenje dêrom earst mei de drill hjirûnder.

1. **Kies it doelsstip**, yn UTC, krekt foar de skea:
   `2026-09-24 09:30:00+00`. It auditlogboek (`/admin/audit`) lit dat momint meastal sjen.
2. **Stopje alles dat skriuwt**:
   `docker compose -f docker/compose.yaml stop web content worker scheduler collab`
3. **Hâld it skansearre cluster** oant de restore kontrolearre is:
   ```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. **Pak de nijste base backup foar it doelsstip út** yn it datavolume en freegje herstel
   nei it opjûne doel oan:
   ```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. **Herstelle**: start Postgres ien kear mei de herstelynstellingen út in Compose-override,
   sadat it gewoane bestân net feroaret:
   ```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]
   ```
   De override ferfangt it hiele kommando, dus werhellet de twa ynstellingen dêr't herstel
   fan ôfhinget: `max_connections` mei net leger wêze as op de primêre database (oars brekt
   herstel ôf mei "insufficient parameter settings") en it mountte `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. **Kontrolearje it** foardatst immen talittest: de auditketen
   (`docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts`)
   en oft de ferlerne gegevens werom binne.
7. **Gean werom nei normaal**: `docker compose -f docker/compose.yaml up -d`. Dit start Postgres
   opnij mei archiving oan; in nije WAL-tiidline begjint. Nim fuort in nije base backup.

De projektnamme `quire` stiet foar elke volumenaam; `docker volume ls` lit de eksakte nammen sjen.

## Plande ferifikaasjedrill <!--quire:the-scheduled-verification-drill-->

`backup-offsite` docht ek elke `QUIRE_BACKUP_DRILL_INTERVAL_HOURS` in drill (standert 168,
wykliks), en op de folgjende trochrin nei in mislearre drill nochris. It hellet de nijste
base backup bûten de host op mei elk WAL-segmint dêrnei; ûntsiferet elk (wat bewiist dat de
kaai it noch iepenet en neat feroare is), ferliket elk bestân mei it manifest, kontrolearret
dat it argyf in Postgres-datamap is en dat der gjin gat sit yn WAL fanôf de backup. It rapport
wurdt as `reports/drill-<time>.json` yn de store en yn it logboek fan de tsjinst skreaun;
by mislearring neamt de drill it bestân of it earste ûntbrekkende segmint.

## De restore drill <!--quire:the-restore-drill-->

In reservekopy dy't nea weromset is, is gjin reservekopy. De drill set de nijste folsleine
base backup fan foar it doelsstip plus it WAL-argyf werom yn in tydlike Postgres dy't neat
mei de live database dielt, en bewiist it resultaat:

```sh
docker/scripts/restore-drill.sh                                  # to ninety minutes ago
docker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"
```

It doelsstip is UTC yn krekt dy foarm. Der moat in âldere base backup wêze en argivearre
WAL dêrnei: nim by in nije ynstallaasje in base backup en wachtsje op it folgjende argivearre
segmint (maksimaal in minút by writes) foardatst in doelsstip nei de backup kiest. De drill
hat allinnich Docker en bash op de host nedich.

Elke stap lit de drill mislearje:

1. **Werom te heljen**: it tydlike cluster spilet op nei it doelsstip en iepenet.
2. **Folslein**: tal rigen fan elke tabel tsjin de live database (`tooling/restore-drill`).
   De live database is sûnt it doelsstip fierder gien, dus kin it tal rigen ferskille mei
   de grutste fan 500 rigen of in tsiende fan de tabelgrutte, yn beide rjochtingen (writes
   meitsje it leger; by wiskjen hat de restore mear). In partition oanmakke nei it doelsstip
   is gjin ûntbrekkende tabel. Ferbreedzje de romte op in drokkere ynstallaasje mei
   `QUIRE_DRILL_MAX_BEHIND` en `QUIRE_DRILL_MAX_DRIFT_RATIO`. In ûntbrekkende of lege tabel
   lit de test mislearje.
3. **Yntakt**: de audit-hashketen wurdt kontrolearre op de weromsette kopy.
4. **Brûkber**: de applikaasjerol kin lêze mei row-level security.
5. **Tiid**: fan start oant grien, neffens `QUIRE_DRILL_RTO_SECONDS` (standert 3600).

It skriuwt nea nei de live database of syn volumes: de reservekopy- en WAL-volumes wurde
allinnich-lêzen mount en it tydlike cluster wurdt oan de ein fuorthelle, oft de drill slagget
of net.

Set `QUIRE_DRILL_REPORT` op in paad om in JSON-rapport (sukses of mislearring) te skriuwen
en plan it út de Docker-host wei:

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

Doch dit alle moannen en foar elke upgrade. In mislearre drill hâldt in upgrade tsjin.
Ien kear it fearnsjier litst ien dy't dizze runbook net skreaun hat in echte point-in-time-
restore dwaan op in ekstra host, mei allinnich dit dokumint.

## Bestannen <!--quire:files-->

Lokale bestannen steane yn it `files`-volume. Meitsje der tagelyk mei de database in
reservekopy fan en set beide tegearre werom:

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

Skeakelje by objektopslach bucketferzjes yn en bewarje net-aktuele ferzjes 35 dagen;
point-in-time-restore fan bestannen wurdt dan troch de bucket sels fersoarge.

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