バックアップとリストア
Notariumのバックアップは、外側から取るファイルコピーではなく、イメージに内蔵されたコマンドです。notarium backup は、サービスが動き続けたまま論理的なZIPを組み立てます。読み取りは終始利用でき、書き込みが抑えられるのは2回の短いチェックポイントの間だけです。必要なのはDockerと稼働中のコンテナだけ — ダウンタイムはありません。
meta.db をコピーしてはいけないcp /data/meta.db はバックアップではありません。メタデータデータベース(メタデータDB)はWALモードで動いており、コミット済みの行がまだ meta.db-wal に残っていることがあります。しかも1つずつコピーしたファイルの寄せ集めは、ある一瞬を切り取ったスナップショットにはなりません。そうしたコピーからリストアしたインスタンスは、黙ってデータを失います。
バックアップを取る
docker compose exec -T notarium backup > notarium-20260731.zip
これは「動くかどうか」の確認ではなく、本物のバックアップです。コマンドはアーカイブを組み立て、自前の検証処理に通し、そのうえで初めてバイト列を流し出します。-T フラグは必須です。付けなければComposeが擬似端末を割り当てますが、バイナリストリームは擬似端末を無傷では通り抜けられず、壊れたアーカイブができあがります。素の docker exec はデフォルトで擬似端末を割り当てないので、そちらではこのフラグは不要です。標準出力はZIPのバイト列だけのために予約されており、進捗と最後のサマリーはstderrへ出るのでアーカイブを汚しません。
定期実行ジョブ向けに
> によるリダイレクトには落とし穴が1つあります。最終的な名前のファイルが、コマンドが何か仕事をする前に作られてしまう、という点です。Dockerかバックアップが途中で落ちれば、正しい名前で中身が不完全なファイルだけが手元に残ります。以下は、失敗しても安全な公開手順です。一時ファイルへ書き、ディスクへフラッシュし、アトミックなハードリンクで公開します。
backup="notarium-$(date -u +%Y%m%dT%H%M%SZ).zip"
partial="${backup}.partial.$$"
set -eu
umask 077
committed=0
cleanup() { test "$committed" -eq 1 || rm -f "$partial"; }
trap cleanup EXIT
docker compose exec -T notarium backup > "$partial"
sync -f "$partial"
sync -f "$(dirname "$backup")"
committed=1
ln "$partial" "$backup"
if sync -f "$(dirname "$backup")"; then
if rm "$partial"; then
sync -f "$(dirname "$backup")" ||
echo "backup warning: final is durable; partial cleanup fsync failed" >&2
else
echo "backup warning: final is durable; retaining recovery partial $partial" >&2
fi
else
echo "backup warning: final is visible; retaining durable recovery partial $partial" >&2
fi
trap - EXIT
set -e は、Dockerかバックアップがエラーを返した場合にアーカイブが公開されるのを防ぎます。一時ファイル名にはPIDが入るので、同時に走る2つのジョブが同じファイルを取り合うことはありません。一時ファイルとそのディレクトリは公開の時点より前にディスクへフラッシュされ、公開そのものは上書きしないアトミックなハードリンクです。同じターゲット名を狙う2つのジョブが互いを潰し合うことはありません。
公開の時点より後の失敗は、誤検知ではなく警告です。最終ファイルはすでに所定の位置にあり、判断がつかない場合には一時ファイルが予備のコピーとして残されます。ln からの曖昧な非ゼロ終了コードでさえ、それを残します — プロセスが中断される直前にリンクが作られていた可能性があるからです。umask 077 は、アーカイブを所有者だけが読める状態にします。一時ファイルと最終ファイルは同じファイルシステム上に置いてください。
素の docker run で notarium という名前で起動したコンテナでも、以上はそのまま当てはまります。変わるのは1行だけです。
docker exec notarium backup > "$partial"
上のラッパーが必要なのは、stdout経由の転送だけではファイルを公開できないからです。アーカイブ用ディレクトリがすでにマウントされているなら — バックアップ共有、NFS、read-only ルートに添えた一時作業用ボリュームなど — 同じ仕事を backup --output /path/archive.zip がこなします。ターゲットの隣の一時ファイルへ書き、ディスクへフラッシュし、検証し、上書きしないアトミックなハードリンクで公開します。失敗した場合、ターゲット名の下には何も残しません。このときシェルのラッパーは不要で、stdoutにはJSONのサマリーが1つだけ流れます。
このコマンドは、別コンテナとしてではなく、すでにサービスを提供しているコンテナの中で docker exec によって実行します。一貫したスナップショットを取るには、稼働中のアプリケーションと足並みを揃える必要があるからです。
バックアップが失敗しうるとき
バックアップは、一貫性のないアーカイブが黙ってできあがることは決してないように作られています。スナップショットを取れなければ、コマンドはエラーで終了し、何も公開しません。それが起きるのは次の2つのケースです。
- 編集が途切れずに流れ込んでいる。 アーカイブを組み立てる間、データは静止していなければなりません。重なった書き込みはリトライを引き起こし、編集が延々と続く状況ではコマンドはエラーで諦めます。実際に出くわすのは活発なインスタンスで、その場合はあとで再実行すれば済みます。
- 長いインポートやエクスポートが走っている。 バックアップには書き込みのごく短い停止が2回必要で、長時間かかるジョブはそこに収まりません。一括インポートと同じ時間帯にバックアップを予定しないでください。
どちらのケースもサービスを傷つけません。書き込みキューは即座に解放され、オペレーター向けコマンドがアプリケーションを待たせ続けることはありません。通常の読み取りは、いずれにせよバックアップの実行中ずっと利用できます。
アーカイブの中身
| アーカイブ内のパス | 中身 |
|---|---|
data/meta.db | アカウント、セッション、メンバーシップ、安定した識別子、バージョン履歴、そしてジョブの状態。 |
data/spaces/ | 信頼できる唯一の情報源であるMarkdown。エージェントメモリとプロジェクトのマーカーファイルも含みます。 |
data/jobs/ | 完成した成果物と、永続化されたインポートのアップロード。 |
manifest.json | フォーマットのバージョン、タイムスタンプ、ディレクトリの正確な集合、そして全ファイルのサイズ・mtime・SHA-256。 |
派生ディレクトリの data/engine/ は含まれません。インデックスはリストア後にファイルから再構築されます。未完了のファイルのうちスキップされるのは内部的なものだけです — ノートのアトミック書き込みの一時ファイル、途中までアップロードされたインポート、エクスポート成果物の断片です。名前が .part で終わる通常のユーザーファイルはアーカイブに残ります。それらは正当なファイルだからです。
このZIPには、メタデータDB由来のアカウントとセッションの状態が入っています。シークレットとして扱ってください。上のスニペットの umask 077 は、新しいアーカイブを所有者だけが読める状態にします。
検証
検証は何も変更しません。そしてすべてのバックアップジョブに組み込むべきものです。
docker compose exec -T notarium backup verify < notarium-20260722.zip
# 素の docker run の場合:
docker exec -i notarium backup verify < notarium-20260722.zip
成功すると、コマンドはJSONのサマリーを1つ出力し、終了コード0で終わります。拒否されるのは、安全でないパスと重複したパス、マニフェストに載っていないファイル、正確に一致しないディレクトリ集合、サイズやハッシュの不一致、不正な時刻メタデータ、上限の超過、そしてSQLiteの整合性チェック失敗です。この処理中、稼働中のデータディレクトリは読み取られも変更されもしません。
ハッシュは偶発的な破損を捕まえますが、改竄からは守ってくれません。内容とマニフェストの両方を差し替えられる者は、検証を通過します。バックアップの保管先は、アクセスを絞った信頼済みの状態とみなすか、アーカイブを運ぶ層のどこかで署名か暗号化を追加してください。
リストア
リストアはオフラインの障害対応オペレーションです。受け付けるのはまっさらな空のデータルートだけで、既存のインスタンスにマージすることも、それを上書きすることも決してありません。
新しいボリュームを用意し、起動前にサービスの向き先をそちらへ切り替えます。
set -eu
docker compose stop notarium
# 古いボリュームは脇へどけ、compose では空の /data をマウントする
docker compose run --rm --no-deps -T notarium restore \
< notarium-20260722.zip
docker compose up -d --force-recreate --no-deps notarium
そのあとコンテナは、単に起動するのではなく作り直す必要があります。docker compose start では、古いマウント設定を抱えた古いコンテナが戻ってきてしまいます。リストアしたインスタンスを確認し終えるまで、古いボリュームは残しておいてください。
リストアは、何かを設置する前にアーカイブ全体を検証します。設置の途中で処理が中断された場合は、明示的なマーカーが残されます。そのターゲットは使い切りとみなし、無理に押し切ったりマージしたりせず、新しい空のターゲットへリストアし直してください。
リストア後に確認すること:
- バックアップに入っていたアカウントでサインインする。
- スペースをいくつか開き、アドレスと識別子が保たれていることを確認する。
- 編集したことのあるノートを開き、その履歴を見る。
- アップロードや成果物が自分にとって重要なインポート・エクスポートのジョブを確認する。
リストアするメタデータDBは、ターゲットのビルドが受け入れるマイグレーション台帳を備えていなければなりません。台帳を持たない空でないデータベースはfail closed(安全側に倒して失敗) します — リストアはそのバージョンを推測しませんし、自分で刻印することもありません。データベースを参照してください。
境界
内蔵コマンドが対応しているのは、正規のレイアウトだけです。単一のデータルートと、SQLiteファイル上のメタデータDBという構成です。META_DB_URL がPostgresを指している場合や、ノートが DATA_DIR の外にある場合、コマンドは部分的なアーカイブを渡す代わりに安全側に倒して失敗します。その場合は、お使いのデータベース自身のツールと、マウント済みディレクトリのスナップショットを併用してください。
バックアップと検証の中間ファイルは、デフォルトでは /tmp に置かれます。ストリーミングのバックアップは公開前に自分自身を検証するため、一時的にアーカイブ1つ分と展開後2ステージ分の空きが必要になることがあります。単体の検証ならアーカイブ+1ステージ分です。リストアは受信ストリームを一時作業領域にバッファしますが、アーカイブの展開先は新しいデータルートそのものです。コンテナのルートファイルシステムがread-onlyでマウントされている場合や、データセットが大きい場合は、NOTARIUM_BACKUP_TMPDIR を書き込み可能なマウント済みディレクトリに向けてください。
圧縮後・展開後の入力はどちらも64GiBと100万エントリが上限で、名前・ZIPの内部構造・manifest.json には32MiBという別枠のメモリ上限があります。信頼できる大規模インストールでは NOTARIUM_BACKUP_MAX_BYTES、NOTARIUM_BACKUP_MAX_ENTRIES、NOTARIUM_BACKUP_MAX_METADATA_BYTES を引き上げられます — 環境変数を参照してください。