コンテンツへスキップ

バックアップ、ポイントインタイム復旧、復元ドリル

Quire をバックアップし、指定時点に復元して、復元ドリルで検証します。

Markdown で表示

設計は docs/architecture/23-ops.md のセクション 8 にあります。Docker Compose 製品の運用手順です。作成者ではない人が読んで実行できるように書かれています。手順が不明瞭であれば、このドキュメントの不備です。

保護対象と方法

資産 方法 保存先
データベース 初回起動から WAL を継続アーカイブ。間隔は最大 60 秒 pgwal ボリューム
データベース 既定では毎日 pg_basebackup でベースバックアップ (backup-scheduler) pgbackup ボリューム
データベース ベースバックアップと WAL を 5 分ごとに暗号化してコピー (backup-offsite) 指定した別ストア
ファイル files ボリューム。ホストのバックアップツールでコピーするか、バージョン管理付きオブジェクトストレージを使う files ボリューム
シークレット docker/.env。特に QUIRE_MASTER_KEY (および利用中の QUIRE_MASTER_KEY_RETIRED)、QUIRE_BACKUP_ENCRYPTION_KEY、docker/secrets/audit-signing-key.pem このホスト以外にもコピーを保管
検索インデックス、キャッシュ、変換済み素材 バックアップ対象外。再構築する

目標は、障害から 60 秒以内の時点に復旧し、500 GB のデータベースを 60 分以内に復元することです。

よくあるミスが 2 つあります。ファイルなしでデータベースを復元すると、ページが壊れます。QUIRE_MASTER_KEY なしで復元すると、保存済みの SSO、webhook、連携の認証情報を復号できません。マスターキーのローテーションが未解決なしで完了するまでは、廃止済みの鍵も必要です (key-rotation.md)。どちらもバックアップ対象です。

バックアップの作成

クラスター全体のベースバックアップ:

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

最新の QUIRE_BACKUP_KEEP 個 (既定値 5) のベースバックアップを保持し、最古のバックアップに不要となった WAL を削除して、アーカイブが無制限に増えないようにします。ホストの cron または systemd timer で毎日実行します。

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

またはスタックのスケジューラーを使います。backup プロファイルは backup-scheduler で QUIRE_BACKUP_INTERVAL_HOURS ごと (既定値 24 時間) にベースバックアップを作成し、次に説明する backup-offsite も起動します。

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

ホスト外への暗号化コピー

2 つのボリュームはデータベースと同じホストにあります。障害が起きたマシン上のバックアップはバックアップではありません。backup-offsite はストレージポートを通じて、すべてのベースバックアップとアーカイブ済み WAL セグメントを暗号化して別ストアにコピーし、保持期間に従って保管します。

  • 暗号化。 QUIRE_BACKUP_ENCRYPTION_KEY (または QUIRE_BACKUP_ENCRYPTION_KEY_FILE で指定したファイル) で AES-256-GCM を使います。鍵は 32 バイトで、openssl rand -hex 32 から生成します。ファイルごとに nonce と認証タグがあり、鍵なしではコピーを読めず、改ざんも検知できます。QUIRE_MASTER_KEY と一緒に鍵を保管しますが、このホストおよびバックアップストアとは別の場所に置いてください。鍵がなければ復元できません。
  • 保存先。 QUIRE_BACKUP_STORAGE_DRIVER は s3、azure、local (マウントしたリモートディスクの場合は QUIRE_BACKUP_STORAGE_ROOT) です。ファイルストレージと同じ設定の先頭に QUIRE_BACKUP_ を付けます。例: QUIRE_BACKUP_S3_ENDPOINT、QUIRE_BACKUP_S3_BUCKET、QUIRE_BACKUP_S3_ACCESS_KEY_ID など。ファイル用とは別のバケットを使い、できれば別のアカウントにします。プロバイダーが対応していれば、書き込みはできても削除はできない認証情報を使います。
  • 保持期間。 最新 QUIRE_BACKUP_OFFSITE_KEEP 個 (既定値は QUIRE_BACKUP_KEEP、未設定なら 7) のベースバックアップと、最古のバックアップに必要な WAL を保持します。それより古いセットとセグメントはストアから削除されます。
  • 間隔。 QUIRE_BACKUP_SHIP_INTERVAL_SECONDS ごと (既定値 300 秒) にコピーします。処理は冪等で、すでに保存済みのものは飛ばします。マニフェストを最後に書き込んだ時点でベースバックアップを保存済みとして扱います。

同じコマンドを手動でも実行できます。

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

新しいホストで復元するには、先にバックアップセットを取得し、以下の手順で pgbackup ボリュームの代わりに取得したディレクトリを、wal-archive の代わりに取得した pgwal を使います。

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

指定時点への復元

インポートの失敗、コースの削除、契約変更のマイグレーションなど、データを失ったときに実行します。稼働中のデータベースを置き換えるため、先に下記のドリルで練習してください。

  1. 損害が発生する直前の目標時刻を UTC で選びます。例: 2026-09-24 09:30:00+00。通常は監査ログ (/admin/audit) で時刻を確認できます。
  2. 書き込みを行うすべてのサービスを停止します: docker compose -f docker/compose.yaml stop web content worker scheduler collab
  3. 復元の確認が終わるまで、損傷したクラスターを残します。
    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. 目標より前に作成された最新のベースバックアップをデータボリュームに展開し、指定時点への復旧を要求します。
    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. 復旧: 通常のファイルに触れないよう Compose のオーバーライドから、復旧設定付きで Postgres を一度起動します。
    # 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]
    オーバーライドはコマンド全体を置き換えるため、復旧に必要な設定を繰り返します。max_connections はプライマリより少なくしないでください (そうでないと接続設定不足で中断します)。マウント済みの pg_hba.conf も指定します。
    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. 利用者を戻す前に確認します。監査チェーンを検証し、失われたデータが戻ったことを確かめます。docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts
  7. 通常運用に戻します: docker compose -f docker/compose.yaml up -d。アーカイブを有効にして Postgres を再起動すると、新しい WAL タイムラインが始まります。すぐに新しいベースバックアップを作成します。

プロジェクト名 quire が各ボリューム名の先頭に付きます。正確な名前は docker volume ls で確認してください。

定期検証ドリル

backup-offsite は QUIRE_BACKUP_DRILL_INTERVAL_HOURS ごとに (既定値 168、週 1 回)、失敗した場合は次の実行でもドリルを行います。ホスト外にある最新ベースバックアップと、その後の WAL セグメントを取得して復号します (鍵で開けることと改ざんがないことを確認)。各ファイルをマニフェストと照合し、アーカイブが Postgres のデータディレクトリであることと、バックアップ以降の WAL に欠落がないことを確かめます。レポートは reports/drill-<time>.json とサービスログに保存されます。失敗時は該当ファイルまたは最初に欠落したセグメントを示します。

復元ドリル

一度も復元していないバックアップは、バックアップとは言えません。このドリルは、目標より前の最新の完全なベースバックアップと WAL アーカイブを、稼働環境とは分離した一時 Postgres に復元して検証します。

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

目標はこの形式で UTC 指定します。その時刻より前のベースバックアップと、その後までのアーカイブ済み WAL が必要です。新規インストールではベースバックアップを作成し、次のアーカイブセグメント (書き込みがあれば最大 1 分) を待ってから、バックアップより後の目標時刻を選んでください。ホストに Docker と bash があれば実行できます。

次の各段階が失敗するとドリル全体が失敗します。

  1. 復旧可能性: 一時クラスターが目標時刻まで再生して起動します。
  2. 完全性: 稼働中データベースと各テーブルの行数を比較します (tooling/restore-drill)。目標より後に稼働中 DB が更新されているため、テーブルごとに行数の 10 分の 1 と 500 行の大きい方まで、増減を許容します (書き込みで復元側が少なくなり、削除で多くなる場合があります)。目標後に作成されたパーティションは欠落扱いになりません。負荷の大きいインストールでは QUIRE_DRILL_MAX_BEHIND と QUIRE_DRILL_MAX_DRIFT_RATIO で許容値を調整してください。テーブルがない、または空の場合は失敗です。
  3. 整合性: 復元コピー上で監査のハッシュチェーンを検証します。
  4. 利用可能性: アプリケーションロールが行レベルセキュリティを通じて読み取れること。
  5. 時間: 開始から成功まで QUIRE_DRILL_RTO_SECONDS (既定値 3600) 以内。

稼働中のデータベースやボリュームには書き込みません。バックアップと WAL のボリュームは読み取り専用でマウントされ、一時クラスターは成否にかかわらず最後に削除されます。

QUIRE_DRILL_REPORT にパスを設定すると、成功時も失敗時も JSON レポートを保存できます。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

毎月、およびアップグレード前に実行してください。ドリルが失敗した場合はアップグレードできません。四半期に 1 度は、この手順を書いていない人に、別のホスト上でこの文書だけを使って実際のポイントインタイム復元をしてもらってください。

ファイル

ローカルのファイルは files ボリュームにあります。データベースと同じ時点でバックアップし、両方を一緒に復元してください。

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 .

オブジェクトストレージを使う場合はバケットのバージョン管理を有効にし、現行でないバージョンを 35 日間保持します。ファイルのポイントインタイム復元はバケット側で行います。

ナビゲーション

入力して検索…

↑↓ 移動↵ 選択Esc 閉じる