---
title: "Backup, point-in-time recovery and the restore drill"
description: "I-backup ang Quire, ibalik ito sa isang tiyak na oras, at patunayan ito sa pamamagitan ng restore drill."
image: "https://docs.quirelms.com/og.png"
---

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

# Backup, point-in-time recovery and the restore drill

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

Ang disenyo ay nasa `docs/architecture/23-ops.md` seksiyon 8. Ito ang runbook
para sa Docker Compose na produkto. Isinulat ito upang masundan ng taong
hindi sumulat nito; kung malabo ang isang hakbang, depekto iyon sa dokumentong ito.

## Ano ang pinoprotektahan, at paano <!--quire:what-is-protected-and-how-->

| Asset | Paano | Saan |
| --- | --- | --- |
| Database | Tuloy-tuloy na ina-archive ang WAL, hindi hihigit sa bawat 60 segundo, mula sa unang boot | `pgwal` volume |
| Database | Mga base backup gamit ang `pg_basebackup`, araw-araw bilang default (`backup-scheduler`) | `pgbackup` volume |
| Database | Mga naka-encrypt na kopya ng mga base backup at WAL, bawat limang minuto (`backup-offsite`) | Hiwalay na store na ikaw ang magtatalaga |
| Mga file | Ang `files` volume. Kopyahin ito gamit ang backup tool ng host mo, o gumamit ng versioned object storage | `files` volume |
| Mga lihim | `docker/.env`, higit sa lahat ang `QUIRE_MASTER_KEY` (at anumang `QUIRE_MASTER_KEY_RETIRED` na ginagamit pa), `QUIRE_BACKUP_ENCRYPTION_KEY`, at `docker/secrets/audit-signing-key.pem` | Magtabi ng kopya sa labas ng host na ito |
| Mga search index, cache, rendition | Hindi bina-backup; muling ginagawa | |

Mga target: recovery point na hindi lalampas sa 60 segundo bago ang pagkabigo,
at restore sa loob ng 60 minuto para sa 500 GB na database.

Dalawang pagkakamali ang karaniwan. Ang database na na-restore **nang wala ang mga file** ay nagdudulot
ng sirang mga pahina. Ang database na na-restore **nang wala ang `QUIRE_MASTER_KEY`** ay hindi
makakapag-decrypt ng mga kredensiyal sa SSO, webhook at integration na laman nito; hanggang sa
matapos ang master key rotation nang walang natitirang hindi nalutas
([key-rotation.md](/fil/ops/key-rotation/)), kasama rito ang mga retiradong key. Bahagi ng backup ang dalawa.

## Pagkuha ng mga backup <!--quire:taking-backups-->

Base backup ng buong cluster:

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

Pinananatili nito ang pinakabagong `QUIRE_BACKUP_KEEP` na mga base backup (default 5) at pinuputol
ang WAL na hindi na kailangan ng pinakaluma, kaya hindi lalaki nang walang hanggan ang archive.
I-iskedyul ito araw-araw gamit ang cron o systemd timer sa 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
```

O hayaang ang stack ang mag-iskedyul nito: pinapatakbo ng `backup` profile ang `backup-scheduler`,
na kumukuha ng base backup bawat `QUIRE_BACKUP_INTERVAL_HOURS` (default 24),
at ang `backup-offsite` na inilalarawan sa susunod.

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

## Mga naka-encrypt na kopyang nasa labas ng host <!--quire:encrypted-off-host-copies-->

Nasa parehong host ng database ang dalawang volume, at hindi maituturing na backup
ang backup na nasa makinang pumalya. Kinokopya ng `backup-offsite` ang bawat base backup
at bawat naka-archive na WAL segment papunta sa hiwalay na store sa pamamagitan ng storage port,
ini-encrypt ito, at pinananatili roon ayon sa retention:

- **Encryption.** AES-256-GCM gamit ang `QUIRE_BACKUP_ENCRYPTION_KEY` (o ang file
  na pinangalanan ng `QUIRE_BACKUP_ENCRYPTION_KEY_FILE`): 32 byte mula sa
  `openssl rand -hex 32`. May sariling nonce at authentication tag ang bawat file,
  kaya hindi mababasa ang kopya kung wala ang key at natutukoy ang anumang pagbabago rito.
  Itabi ang key kasama ng `QUIRE_MASTER_KEY`, malayo sa host na ito at sa backup store.
  Kung wala ang key, walang maibabalik.
- **Saan.** `QUIRE_BACKUP_STORAGE_DRIVER` ay `s3`, `azure` o `local` (isang naka-mount
  na remote disk sa `QUIRE_BACKUP_STORAGE_ROOT`). Mga setting ito ng file storage na may
  `QUIRE_BACKUP_` na prefix: `QUIRE_BACKUP_S3_ENDPOINT`, `QUIRE_BACKUP_S3_BUCKET`,
  `QUIRE_BACKUP_S3_ACCESS_KEY_ID`, at iba pa. Gumamit ng ibang bucket at, kung maaari,
  ibang account kaysa sa mga file, na may mga kredensiyal na puwedeng magsulat pero hindi
  magtanggal kung pinapayagan ito ng provider.
- **Retention.** Pinakabagong `QUIRE_BACKUP_OFFSITE_KEEP` na mga base backup (default
  `QUIRE_BACKUP_KEEP`, o 7 kung wala ito) at ang WAL na kailangan ng pinakaluma sa mga ito;
  tatanggalin sa store ang mas lumang mga set at segment.
- **Kailan.** Bawat `QUIRE_BACKUP_SHIP_INTERVAL_SECONDS` (default 300). Idempotent ang pagpapadala:
  nilalaktawan ang naka-store na, at maituturing lang na naka-store ang base backup kapag naisulat
  na ang manifest nito, bilang huling hakbang.

Manu-mano ring pinapatakbo ang parehong command:

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

Para mag-restore sa bagong host, kunin muna roon ang isang set, saka sundin ang mga hakbang sa ibaba
gamit ang nakuha mong directory kapalit ng `pgbackup` volume at ang nakuha mong `wal-archive`
kapalit ng `pgwal`:

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

## Pag-restore sa isang tiyak na oras <!--quire:restoring-to-a-point-in-time-->

Gamitin ito pagkatapos mawalan ng data: maling import, naburang kurso, o paglipat ng kontrata
na kailangan mong bawiin. Pinapalitan nito ang live na database, kaya magsanay muna rito
gamit ang drill sa ibaba.

1. **Piliin ang target na oras**, sa UTC, bago mismo ang pinsala:
   `2026-09-24 09:30:00+00`. Karaniwang ipinapakita ng audit log (`/admin/audit`) ang
   sandaling iyon.
2. **Ihinto ang lahat ng sumusulat**:
   `docker compose -f docker/compose.yaml stop web content worker scheduler collab`
3. **Panatilihin ang napinsalang cluster** hanggang ma-verify ang restore:
   ```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. **I-unpack sa data volume ang pinakabagong base backup na mas luma sa target**, at humiling ng
   recovery sa tinukoy na oras:
   ```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. **Mag-recover**: minsanang simulan ang Postgres gamit ang mga setting ng recovery mula sa
   Compose override para manatiling hindi nagbabago ang karaniwang file:
   ```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]
   ```
   Pinapalitan ng override ang buong command, kaya inuulit nito ang dalawang setting na kailangan
   ng recovery: `max_connections` na hindi mas mababa sa primary (kung hindi, hihinto ang recovery
   dahil sa "insufficient parameter settings") at ang naka-mount na `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. **Suriin ito** bago papasukin ang sinuman: ang audit chain
   (`docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts`),
   at tiyaking naibalik ang nawalang data.
7. **Bumalik sa normal**: `docker compose -f docker/compose.yaml up -d`. Magsisimulang muli ang
   Postgres na naka-on ang archiving, at magsisimula ang bagong WAL timeline. Kumuha agad ng bagong
   base backup.

Nilalagyan ng pangalan ng proyekto na `quire` ang bawat volume; ipinapakita ng `docker volume ls`
ang eksaktong mga pangalan.

## Naka-iskedyul na verification drill <!--quire:the-scheduled-verification-drill-->

Nagpapatakbo rin ng drill ang `backup-offsite` bawat `QUIRE_BACKUP_DRILL_INTERVAL_HOURS`
(default 168, lingguhan), at muli sa susunod na pagtakbo kapag pumalya ang isa. Kinukuha nito
ang pinakabagong off-host base backup at bawat WAL segment pagkatapos nito, dine-decrypt ang bawat
isa (na nagpapatunay na nabubuksan pa rin ng key ang mga ito at walang binago), inihahambing ang
bawat file sa manifest nito, sinusuri kung Postgres data directory ang archive, at tinitiyak na
walang puwang sa WAL mula sa backup pataas. Isinusulat ang ulat sa store bilang
`reports/drill-<time>.json` at sa log ng serbisyo; tinutukoy ng pumalyang drill ang file o ang
unang nawawalang segment.

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

Hindi maituturing na backup ang backup na hindi pa na-restore. Ibinabalik ng drill ang
pinakabagong kumpletong base backup na kinuha bago ang target, kasama ang WAL archive,
sa isang pansamantalang Postgres na walang anumang ibinabahagi sa live na database, at
pinatutunayan ang resulta:

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

UTC ang target at dapat eksaktong ganito ang format. Kailangan nito ng base backup na mas luma rito
at naka-archive na WAL na lampas dito: sa bagong install, kumuha ng base backup at hintayin ang
susunod na naka-archive na segment (hindi hihigit sa isang minuto kapag may mga write) bago pumili
ng target na pagkatapos ng backup. Docker at bash lang sa host ang kailangan ng drill.

Ang bawat hakbang na ito ay nagpapabagsak sa drill:

1. **Kakayahang mag-recover**: nare-replay ng pansamantalang cluster ang mga log hanggang sa target at nagbubukas ito.
2. **Pagiging kumpleto**: ihambing ang bilang ng row ng bawat talahanayan sa live na database
   (`tooling/restore-drill`). Nagpatuloy na ang pagbabago sa live na database mula sa target,
   kaya maaaring magkaiba ang isang talahanayan nang hanggang sa mas malaki sa 500 row o ikasampu
   ng laki nito, sa alinmang direksiyon (napag-iiwanan ito ng mga write, samantalang mas marami
   ang nananatili sa restore kapag may mga deletion); hindi nawawalang talahanayan ang partition
   na ginawa pagkatapos ng target. Palawakin ang allowance sa mas abalang install gamit ang
   `QUIRE_DRILL_MAX_BEHIND` at `QUIRE_DRILL_MAX_DRIFT_RATIO`. Nabibigo ito kapag may nawawala o
   naubos na talahanayan.
3. **Integridad**: nabe-verify ang audit hash chain sa na-restore na kopya.
4. **Pagiging magagamit**: nakakabasa ang application role gamit ang row-level security.
5. **Oras**: mula simula hanggang maging berde, ayon sa `QUIRE_DRILL_RTO_SECONDS`
   (default 3600).

Hindi ito kailanman sumusulat sa live na database o mga volume nito: read-only ang pagkaka-mount
ng mga backup at WAL volume at inaalis ang pansamantalang cluster sa katapusan, pumasa man o bumagsak.

Itakda ang `QUIRE_DRILL_REPORT` sa isang path upang maisulat doon ang JSON report, pumasa man o
bumagsak, at patakbuhin ito ayon sa iskedyul mula sa Docker host:

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

Patakbuhin ito buwan-buwan at bago ang bawat upgrade. Pinipigilan ng pumalyang drill ang upgrade.
Kada quarter, magpatest ng tunay na point-in-time restore sa ekstrang host sa taong hindi sumulat
ng runbook na ito, gamit lamang ang dokumentong ito.

## Mga file <!--quire:files-->

Nasa `files` volume ang mga lokal na file. I-backup ito kasabay ng database, sa parehong oras,
at i-restore ang dalawa nang magkasama:

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

Sa object storage, i-on ang bucket versioning at panatilihin nang 35 araw ang mga hindi na
pinakabagong bersiyon; ang bucket mismo ang bahala sa point-in-time recovery ng mga file.

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