---
title: "Cadangan, pemulihan ke titik waktu tertentu, dan latihan pemulihan"
description: "Cadangkan Quire, pulihkan ke titik waktu tertentu, lalu buktikan prosesnya dengan latihan pemulihan."
image: "https://docs.quirelms.com/og.png"
---

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

# Cadangan, pemulihan ke titik waktu tertentu, dan latihan pemulihan

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

Rancangannya dijelaskan di bagian 8 `docs/architecture/23-ops.md`. Ini adalah
runbook untuk produk Docker Compose. Dokumen ini ditulis agar dapat diikuti oleh
seseorang yang tidak menulisnya; jika ada langkah yang tidak jelas, berarti ada
kekurangan dalam dokumen ini.

## Apa yang dilindungi dan bagaimana caranya <!--quire:what-is-protected-and-how-->

| Aset | Cara | Lokasi |
| --- | --- | --- |
| Basis data | WAL diarsipkan terus-menerus, paling lama setiap 60 detik, sejak boot pertama | Volume `pgwal` |
| Basis data | Cadangan dasar dengan `pg_basebackup`, secara bawaan setiap hari (`backup-scheduler`) | Volume `pgbackup` |
| Basis data | Salinan terenkripsi dari cadangan dasar dan WAL, setiap lima menit (`backup-offsite`) | Penyimpanan terpisah yang Anda tentukan |
| Berkas | Volume `files`. Salin dengan alat pencadangan host Anda, atau gunakan penyimpanan objek berversi | Volume `files` |
| Rahasia | `docker/.env`, terutama `QUIRE_MASTER_KEY` (dan `QUIRE_MASTER_KEY_RETIRED` yang masih digunakan), `QUIRE_BACKUP_ENCRYPTION_KEY`, serta `docker/secrets/audit-signing-key.pem` | Simpan salinannya di luar host ini |
| Indeks pencarian, cache, hasil olahan | Tidak dicadangkan; dibuat ulang | |

Sasaran: titik pemulihan dalam rentang 60 detik sebelum kegagalan, dan pemulihan
dalam 60 menit untuk basis data 500 GB.

Dua kesalahan umum: basis data yang dipulihkan **tanpa berkasnya** akan
menampilkan halaman rusak. Basis data yang dipulihkan **tanpa
`QUIRE_MASTER_KEY`** tidak dapat mendekripsi kredensial SSO, webhook, dan
integrasi yang tersimpan di dalamnya. Sampai rotasi kunci utama selesai tanpa
nilai yang belum terselesaikan ([key-rotation.md](/id/ops/key-rotation/)), hal ini juga
mencakup kunci yang dipensiunkan. Keduanya merupakan bagian dari cadangan.

## Membuat cadangan <!--quire:taking-backups-->

Cadangan dasar seluruh klaster:

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

Proses ini menyimpan `QUIRE_BACKUP_KEEP` cadangan dasar terbaru (bawaan 5) dan
menghapus WAL tertua yang sudah tidak diperlukan, sehingga arsip tidak tumbuh
tanpa batas. Jadwalkan setiap hari dengan cron atau pewaktu systemd di 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
```

Atau biarkan tumpukan menjadwalkannya: profil `backup` menjalankan
`backup-scheduler`, yang membuat cadangan dasar setiap
`QUIRE_BACKUP_INTERVAL_HOURS` (bawaan 24), serta `backup-offsite` yang dijelaskan
berikutnya.

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

## Salinan terenkripsi di luar host <!--quire:encrypted-off-host-copies-->

Kedua volume berada di host yang sama dengan basis data, dan cadangan di mesin
yang mengalami kegagalan bukanlah cadangan. `backup-offsite` menyalin setiap
cadangan dasar dan setiap segmen WAL yang diarsipkan ke penyimpanan terpisah
melalui port penyimpanan, mengenkripsinya, lalu menyimpannya dengan kebijakan
retensi:

- **Enkripsi.** AES-256-GCM dengan `QUIRE_BACKUP_ENCRYPTION_KEY` (atau berkas
  yang ditunjuk oleh `QUIRE_BACKUP_ENCRYPTION_KEY_FILE`): 32 byte, dibuat dengan
  `openssl rand -hex 32`. Setiap berkas memiliki nonce dan tag autentikasinya
  sendiri, sehingga salinan tidak dapat dibaca tanpa kunci dan setiap perubahan
  akan terdeteksi. Simpan kunci bersama `QUIRE_MASTER_KEY`, jauh dari host ini
  dan jauh dari penyimpanan cadangan. Tanpa kunci, pemulihan tidak mungkin
  dilakukan.
- **Lokasi.** `QUIRE_BACKUP_STORAGE_DRIVER` bernilai `s3`, `azure`, atau `local`
  (disk jarak jauh yang dipasang di `QUIRE_BACKUP_STORAGE_ROOT`). Pengaturannya
  sama dengan pengaturan penyimpanan berkas, tetapi memakai awalan
  `QUIRE_BACKUP_`: `QUIRE_BACKUP_S3_ENDPOINT`, `QUIRE_BACKUP_S3_BUCKET`,
  `QUIRE_BACKUP_S3_ACCESS_KEY_ID`, dan seterusnya. Gunakan bucket yang berbeda,
  dan idealnya akun yang berbeda dari penyimpanan berkas; gunakan kredensial yang
  dapat menulis tetapi tidak menghapus jika penyedia mengizinkannya.
- **Retensi.** Cadangan dasar terbaru sebanyak `QUIRE_BACKUP_OFFSITE_KEEP`
  (bawaan `QUIRE_BACKUP_KEEP`, atau 7 jika tidak disetel) dan WAL yang diperlukan
  oleh cadangan paling lama di antaranya; set dan segmen yang lebih lama dihapus
  dari penyimpanan.
- **Waktu.** Setiap `QUIRE_BACKUP_SHIP_INTERVAL_SECONDS` (bawaan 300 detik).
  Pengiriman bersifat idempoten: data yang sudah tersimpan dilewati, dan
  cadangan dasar baru dianggap tersimpan setelah manifesnya ditulis sebagai
  langkah terakhir.

Perintah yang sama dapat dijalankan secara manual:

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

Untuk memulihkan di host baru, tarik satu set cadangan terlebih dahulu, lalu
ikuti langkah-langkah di bawah dengan direktori hasil pengambilan sebagai
pengganti volume `pgbackup` dan direktori hasil pengambilan `wal-archive`
sebagai pengganti `pgwal`:

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

## Memulihkan ke titik waktu tertentu <!--quire:restoring-to-a-point-in-time-->

Gunakan prosedur ini setelah kehilangan data: impor yang keliru, kursus yang
terhapus, atau migrasi kontrak yang perlu dibatalkan. Prosedur ini mengganti
basis data aktif, jadi latih dahulu menggunakan latihan di bawah.

1. **Pilih waktu sasaran**, dalam UTC, sesaat sebelum kerusakan terjadi:
   `2026-09-24 09:30:00+00`. Log audit (`/admin/audit`) biasanya menunjukkan
   waktunya.
2. **Hentikan semua proses yang menulis**:
   `docker compose -f docker/compose.yaml stop web content worker scheduler collab`
3. **Pertahankan klaster yang rusak** sampai pemulihan terverifikasi:
   ```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. **Bongkar cadangan dasar terbaru yang lebih lama dari waktu sasaran** ke
   volume data, lalu minta pemulihan terarah:
   ```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. **Pulihkan**: jalankan Postgres sekali dengan pengaturan pemulihan, melalui
   override Compose agar berkas normal tidak tersentuh:
   ```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 ini mengganti seluruh perintah, jadi dua pengaturan yang diperlukan
   untuk pemulihan harus dicantumkan kembali: `max_connections` tidak boleh
   lebih rendah daripada milik primer (jika tidak, pemulihan berhenti dengan
   pesan "insufficient parameter settings") dan `pg_hba.conf` yang dipasang.
   ```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. **Periksa hasilnya** sebelum mengizinkan siapa pun masuk: rantai audit
   (`docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts`),
   dan pastikan data yang hilang sudah kembali.
7. **Kembali ke operasi normal**: `docker compose -f docker/compose.yaml up -d`.
   Postgres akan dimulai ulang dengan pengarsipan aktif, dan linimasa WAL baru
   dimulai. Segera buat cadangan dasar yang baru.

Nama proyek `quire` menjadi awalan setiap volume; `docker volume ls` menampilkan
nama persisnya.

## Latihan verifikasi terjadwal <!--quire:the-scheduled-verification-drill-->

`backup-offsite` juga menjalankan latihan setiap
`QUIRE_BACKUP_DRILL_INTERVAL_HOURS` (bawaan 168, mingguan), dan sekali lagi pada
putaran berikutnya jika terjadi kegagalan. Proses ini mengambil cadangan dasar
terbaru dari luar host dan setiap segmen WAL setelahnya, mendekripsi semuanya
(membuktikan kuncinya masih dapat membukanya dan tidak ada yang diubah),
membandingkan setiap berkas dengan manifesnya, memeriksa bahwa arsip merupakan
direktori data Postgres, serta memastikan WAL sejak cadangan tidak memiliki jeda.
Laporan ditulis ke penyimpanan sebagai `reports/drill-<time>.json` dan ke log
layanan; latihan yang gagal menyebutkan berkas atau segmen pertama yang hilang.

## Latihan pemulihan <!--quire:the-restore-drill-->

Cadangan yang belum pernah dipulihkan bukanlah cadangan. Latihan ini memulihkan
cadangan dasar lengkap terbaru sebelum waktu sasaran beserta arsip WAL ke Postgres
sementara yang tidak berbagi apa pun dengan instans aktif, lalu membuktikan
hasilnya:

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

Waktu sasaran harus UTC dengan format persis seperti itu. Diperlukan cadangan
dasar yang lebih lama darinya dan WAL yang diarsipkan melewati waktu tersebut:
pada instalasi baru, buat cadangan dasar dan tunggu segmen berikutnya diarsipkan
(paling lama satu menit jika ada penulisan) sebelum memilih waktu sasaran setelah
cadangan itu. Latihan ini hanya memerlukan Docker dan bash di host.

Setiap langkah berikut dapat menggagalkan latihan:

1. **Dapat dipulihkan**: klaster sementara memutar ulang WAL hingga waktu sasaran
   dan dapat dibuka.
2. **Lengkap**: jumlah baris setiap tabel dibandingkan dengan basis data aktif
   (`tooling/restore-drill`). Basis data aktif sudah berubah sejak waktu sasaran,
   jadi perbedaan tabel dapat mencapai nilai yang lebih besar antara 500 baris
   dan sepersepuluh ukurannya, ke arah mana pun (penulisan membuatnya tertinggal,
   penghapusan membuat hasil pemulihan memiliki lebih banyak baris); partisi yang
   dibuat setelah waktu sasaran bukanlah tabel yang hilang. Pada instalasi yang
   lebih sibuk, perluas toleransi dengan `QUIRE_DRILL_MAX_BEHIND` dan
   `QUIRE_DRILL_MAX_DRIFT_RATIO`. Tabel yang hilang atau kosong akan menggagalkan
   latihan.
3. **Integritas**: rantai hash audit terverifikasi pada salinan yang dipulihkan.
4. **Dapat digunakan**: peran aplikasi dapat membaca melalui keamanan tingkat
   baris.
5. **Waktu**: dari mulai hingga hasil hijau, dibandingkan dengan
   `QUIRE_DRILL_RTO_SECONDS` (bawaan 3600).

Latihan ini tidak pernah menulis ke basis data aktif ataupun volumenya: volume
cadangan dan WAL dipasang hanya-baca dan klaster sementara dihapus pada akhir
proses, baik berhasil maupun gagal.

Setel `QUIRE_DRILL_REPORT` ke suatu lokasi agar laporan JSON ditulis, baik
berhasil maupun gagal, lalu jalankan secara terjadwal dari host Docker:

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

Jalankan setiap bulan dan sebelum setiap peningkatan versi. Latihan yang gagal
menghalangi peningkatan versi. Setiap tiga bulan sekali, minta seseorang yang
tidak menulis runbook ini melakukan pemulihan titik waktu sungguhan di host
cadangan dengan hanya menggunakan dokumen ini.

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

Berkas lokal berada di volume `files`. Cadangkan bersamaan dengan basis data,
pada waktu yang sama, dan pulihkan keduanya bersama-sama:

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

Untuk penyimpanan objek, aktifkan pembuatan versi bucket dan simpan versi lama
selama 35 hari; pemulihan titik waktu untuk berkas kemudian ditangani oleh
bucket itu sendiri.

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