---
title: "Backup, gendannelse til et bestemt tidspunkt og gendannelsesøvelse"
description: "Tag backup af Quire, gendan til et bestemt tidspunkt, og bevis det med gendannelsesøvelsen."
image: "https://docs.quirelms.com/og.png"
---

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

# Backup, gendannelse til et bestemt tidspunkt og gendannelsesøvelse

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

Designet beskrives i afsnit 8 i `docs/architecture/23-ops.md`. Dette er driftsvejledningen
til Docker Compose-produktet. Den er skrevet, så en person, der ikke har skrevet den,
kan følge den; hvis et trin er uklart, er det en fejl i dette dokument.

## Hvad der beskyttes, og hvordan <!--quire:what-is-protected-and-how-->

| Aktiv | Metode | Placering |
| --- | --- | --- |
| Databasen | WAL arkiveres løbende, højst hvert 60. sekund, fra første opstart | `pgwal`-volumen |
| Databasen | Grundbackups med `pg_basebackup`, som standard dagligt (`backup-scheduler`) | `pgbackup`-volumen |
| Databasen | Krypterede kopier af grundbackups og WAL, hvert femte minut (`backup-offsite`) | Et særskilt lager, du angiver |
| Filer | `files`-volumen. Kopiér den med værtens backuptool, eller brug objektlagring med versionering | `files`-volumen |
| Hemmeligheder | `docker/.env`, især `QUIRE_MASTER_KEY` (og enhver `QUIRE_MASTER_KEY_RETIRED`, der stadig er i brug), `QUIRE_BACKUP_ENCRYPTION_KEY` og `docker/secrets/audit-signing-key.pem` | Gem en kopi uden for denne vært |
| Søgeindekser, cacher, visninger | Sikkerhedskopieres ikke; de genopbygges | |

Mål: et gendannelsespunkt højst 60 sekunder før fejlen og gendannelse
inden for 60 minutter for en database på 500 GB.

To fejl går ofte igen. En database, der gendannes **uden sine filer**, viser
ødelagte sider. En database, der gendannes **uden `QUIRE_MASTER_KEY`**, kan ikke
dekryptere SSO-, webhook- og integrationsoplysningerne, den indeholder. Indtil en rotation af hovednøglen
er gennemført uden uløste værdier ([key-rotation.md](/da/ops/key-rotation/)),
omfatter det også de udfasede nøgler. Begge dele er en del af backuppen.

## Tag backup <!--quire:taking-backups-->

En grundbackup af hele klyngen:

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

Den beholder de nyeste `QUIRE_BACKUP_KEEP` grundbackups (standard 5) og sletter
WAL, som den ældste ikke længere behøver, så arkivet ikke vokser uden grænser.
Planlæg den dagligt med cron eller en systemd-timer på værten:

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

Eller lad stacken planlægge det: profilen `backup` starter `backup-scheduler`,
der tager en grundbackup hver `QUIRE_BACKUP_INTERVAL_HOURS` (standard 24),
og `backup-offsite`, som beskrives næste gang.

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

## Krypterede kopier uden for værten <!--quire:encrypted-off-host-copies-->

Begge volumener ligger på samme vært som databasen, og en backup på
maskinen, der fejlede, er ingen backup. `backup-offsite` kopierer hver grundbackup
og hvert arkiveret WAL-segment til et separat lager via lagerporten,
krypterer dem og opbevarer dem efter en opbevaringspolitik:

- **Kryptering.** AES-256-GCM med `QUIRE_BACKUP_ENCRYPTION_KEY` (eller filen
  angivet af `QUIRE_BACKUP_ENCRYPTION_KEY_FILE`): 32 byte fra
  `openssl rand -hex 32`. Hver fil har sin egen nonce og en autentificeringstag,
  så kopien ikke kan læses uden nøglen, og enhver ændring opdages. Opbevar nøglen sammen med
  `QUIRE_MASTER_KEY`, adskilt fra denne vært og backup-lageret. Uden nøglen kan intet gendannes.
- **Placering.** `QUIRE_BACKUP_STORAGE_DRIVER` er `s3`, `azure` eller `local` (en
  monteret ekstern disk under `QUIRE_BACKUP_STORAGE_ROOT`). Indstillingerne er de samme som til fillagring, blot med præfikset `QUIRE_BACKUP_`: `QUIRE_BACKUP_S3_ENDPOINT`,
  `QUIRE_BACKUP_S3_BUCKET`, `QUIRE_BACKUP_S3_ACCESS_KEY_ID` osv. Brug en anden bucket og helst en anden konto end til filerne, med
  legitimationsoplysninger, der kan skrive, men ikke slette, hvis udbyderen tillader det.
- **Opbevaring.** De nyeste `QUIRE_BACKUP_OFFSITE_KEEP` grundbackups (standard
  `QUIRE_BACKUP_KEEP`, ellers 7) og den WAL, som den ældste behøver; ældre
  sæt og segmenter slettes fra lageret.
- **Tidspunkt.** Hver `QUIRE_BACKUP_SHIP_INTERVAL_SECONDS` (standard 300). Overførslen
  er idempotent: Det, der allerede findes, springes over, og en grundbackup regnes først som overført,
  når manifestet er skrevet til sidst.

Den samme kommando kan køres manuelt:

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

Hvis du vil gendanne på en ny vært, skal du først hente et sæt. Følg derefter trinnene nedenfor
med den hentede mappe i stedet for `pgbackup`-volumen og det hentede
`wal-archive` i stedet for `pgwal`:

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

## Gendan til et bestemt tidspunkt <!--quire:restoring-to-a-point-in-time-->

Brug dette efter datatab: en fejlbehæftet import, et slettet kursus eller en kontraktions-
migrering, du har brug for at fortryde. Det erstatter databasen, der er i drift, så prøv det først
med øvelsen nedenfor.

1. **Vælg måltidspunktet** i UTC, lige før skaden:
   `2026-09-24 09:30:00+00`. Auditloggen (`/admin/audit`) viser normalt
   tidspunktet.
2. **Stop alle processer, der skriver**:
   `docker compose -f docker/compose.yaml stop web content worker scheduler collab`
3. **Behold den beskadigede klynge**, indtil gendannelsen er kontrolleret:
   ```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 den nyeste grundbackup, der ligger før måltidspunktet, ud** i datavolumen,
   og anmod om målrettet gendannelse:
   ```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. **Gendan**: Start Postgres én gang med gendannelsesindstillingerne fra en
   Compose-override, så den almindelige fil ikke ændres:
   ```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]
   ```
   Override-filen erstatter hele kommandoen, så den gentager de to indstillinger,
   gendannelsen afhænger af: `max_connections`, som ikke må være lavere end den primæres
   (ellers afbrydes gendannelsen med "insufficient parameter settings"), og den monterede `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. **Kontrollér databasen**, før nogen får adgang: auditkæden
   (`docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts`)
   og at de manglende data er tilbage.
7. **Gå tilbage til normal drift**: `docker compose -f docker/compose.yaml up -d`. Det
   genstarter Postgres med arkivering slået til, og en ny WAL-tidslinje begynder. Tag straks en ny grundbackup.

Projektnavnet `quire` føjes foran hver volumen; `docker volume ls` viser de
præcise navne.

## Den planlagte kontroløvelse <!--quire:the-scheduled-verification-drill-->

`backup-offsite` kører også en øvelse hver `QUIRE_BACKUP_DRILL_INTERVAL_HOURS`
(standard 168, ugentligt) og igen ved næste gennemgang efter en fejl. Den henter
den nyeste offsite-grundbackup og alle WAL-segmenter efter den, dekrypterer hver fil (hvilket beviser,
at nøglen stadig kan åbne dem, og at de ikke er ændret), sammenligner hver
fil med dens manifest, kontrollerer, at arkivet er en Postgres-datamappe, og
kontrollerer, at WAL fra backuppen og frem ikke mangler segmenter. Rapporten skrives til lageret som `reports/drill-<time>.json` og til tjenestens log. En mislykket øvelse angiver filen eller det første manglende segment.

## Gendannelsesøvelsen <!--quire:the-restore-drill-->

En backup, der aldrig er blevet gendannet, er ingen backup. Øvelsen gendanner
den nyeste komplette grundbackup fra før måltidspunktet samt WAL-arkivet
til en separat Postgres-scratchinstans, der ikke deler noget med den aktive instans, og dokumenterer resultatet:

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

Målet er UTC i præcis dette format. Der kræves en grundbackup fra før målet
og arkiveret WAL efter det: På en ny installation skal du tage en grundbackup og vente på
næste arkiverede segment (højst ét minut med skrivninger), før du vælger et
tidspunkt efter backuppen. Øvelsen kræver Docker og bash på værten, intet andet.

Hvert trin kan få øvelsen til at fejle:

1. **Gendannelsesevne**: Scratch-klyngen afspiller til målet og åbner.
2. **Fuldstændighed**: Antal rækker i hver tabel sammenlignes med den aktive database
   (`tooling/restore-drill`). Den aktive database er ændret siden måltidspunktet,
   så en tabel må afvige med højst den største af 500 rækker og en tiendedel af dens størrelse, i
   begge retninger (skrivninger gør den gendannede database bagefter, sletninger betyder, at gendannelsen indeholder flere rækker);
   en partition oprettet efter målet er ikke en manglende tabel. Forøg
   tolerancen på en travlere installation med `QUIRE_DRILL_MAX_BEHIND` og
   `QUIRE_DRILL_MAX_DRIFT_RATIO`. En manglende eller tømt tabel medfører fejl.
3. **Integritet**: Audit-hashkæden valideres på den gendannede kopi.
4. **Anvendelighed**: Applikationsrollen kan læse data via row-level security.
5. **Tid**: Tiden fra start til godkendt resultat holdes op mod `QUIRE_DRILL_RTO_SECONDS`
   (standard 3600).

Øvelsen skriver aldrig til den aktive database eller dens volumener: backup- og WAL-
volumenerne monteres skrivebeskyttet, og scratch-klyngen fjernes til sidst,
uanset om den består eller fejler.

Angiv `QUIRE_DRILL_REPORT` til en sti for at få skrevet en JSON-rapport, uanset om øvelsen
består eller fejler, og kør den efter en tidsplan fra Docker-værten:

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

Kør den hver måned og før hver opgradering. En mislykket øvelse blokerer opgraderingen.
En gang i kvartalet bør en person, der ikke har skrevet denne vejledning, udføre en reel
gendannelse til et bestemt tidspunkt på en reservevært kun ved hjælp af dette dokument.

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

Lokale filer ligger i `files`-volumen. Tag backup af den samtidig med databasen,
og gendan begge sammen:

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

Hvis du bruger objektlagring, skal du aktivere versionering af bucketten og beholde tidligere versioner i 35 dage; gendannelse af filer til et bestemt tidspunkt håndteres da af buckettens egen funktion.

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