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
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),
omfatter det også de udfasede nøgler. Begge dele er en del af backuppen.
Tag backup
En grundbackup af hele klyngen:
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:
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.
docker compose -f docker/compose.yaml --profile backup up -d
Krypterede kopier uden for værten
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:
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
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:
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore
Gendan til et bestemt tidspunkt
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.
Vælg måltidspunktet i UTC, lige før skaden:
2026-09-24 09:30:00+00. Auditloggen (/admin/audit) viser normalt
tidspunktet.
Stop alle processer, der skriver:
docker compose -f docker/compose.yaml stop web content worker scheduler collab
Behold den beskadigede klynge, indtil gendannelsen er kontrolleret:
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/
Pak den nyeste grundbackup, der ligger før måltidspunktet, ud i datavolumen,
og anmod om målrettet gendannelse:
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'
Gendan: Start Postgres én gang med gendannelsesindstillingerne fra en
Compose-override, så den almindelige fil ikke ændres:
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.
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"
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.
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
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
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:
docker/scripts/restore-drill.sh # to ninety minutes agodocker/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:
Gendannelsesevne: Scratch-klyngen afspiller til målet og åbner.
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.
Integritet: Audit-hashkæden valideres på den gendannede kopi.
Anvendelighed: Applikationsrollen kan læse data via row-level security.
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:
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
Lokale filer ligger i files-volumen. Tag backup af den samtidig med databasen,
og gendan begge sammen:
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.