Lewati ke konten

Cadangan, pemulihan ke titik waktu tertentu, dan latihan pemulihan

Cadangkan Quire, pulihkan ke titik waktu tertentu, lalu buktikan prosesnya dengan latihan pemulihan.

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

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), hal ini juga mencakup kunci yang dipensiunkan. Keduanya merupakan bagian dari cadangan.

Membuat cadangan

Cadangan dasar seluruh klaster:

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:

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.

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

Salinan terenkripsi di luar host

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:

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:

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

Memulihkan ke titik waktu tertentu

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:
    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:
    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:
    # 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.
    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

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

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:

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:

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

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

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.

Navigasi

Ketik untuk mencari…

↑↓ navigasi↵ pilihEsc tutup