開発中のプラグインのフォルダが壊れました。restic は 6 時間おきに動かしていたのに、戻せたのは 12 時間前の状態でした。半日ぶんの作業が消えたわけです。
原因を追いかけたら、restic そのものではなく、その周りに 3 つ問題がありました。実行間隔、保持ポリシー、そして失敗に気づく仕組みです。この 3 つは別々に見えて、どれか 1 つだけ直しても効きません。

上が今回いちばん効いた発見です。--keep-daily 7 だけを指定していると、同じ日に何本スナップショットを作っても、残るのはその日の最後の 1 本だけになります。間隔を 30 分に縮めても、1 日 48 本作って 47 本が即日消えます。半日前の状態しか戻せなかったのは、これが理由の半分でした。

足したのが --keep-within 2d です。直近 2 日ぶんは日次の間引きを通さず、全部そのまま残します。30 分間隔なら 96 本ほど。restic は重複排除が効くので、容量はほとんど増えません。
環境は macOS 26 Tahoe、restic 0.19.1、バックアップ先は SFTP 経由の NAS です。
8/13 17:02、最後のスナップショット
まず状況を確認しようと restic forget --dry-run を叩きました。ポリシーの効き方を見るつもりだったのですが、目に入ったのは別のものでした。
いちばん新しいスナップショットの日付が 8/13 17:02 で止まっていました。その日は 8/15 です。6 時間間隔なら、この間に 7 本か 8 本できていないとおかしい。
決め手になったのは、daily 枠の並びでした。--keep-daily 7 で残る 7 本が 8/7 から 8/13 で終わっていました。動いていれば 8/9 から 8/15 になっているはずです。
保持ポリシーの出力は、バックアップが生きているかどうかの健康診断としても読めます。daily 枠の末尾が今日でないなら、そこで止まっています。
余談ですが、これは 2 度目でした。7 月にも 4 日間止まったことがあって、そのときも気づいたのは復旧のためにリポジトリを開いたときです。同じ踏み方を 2 回している。
8/15 11:53、274 バイトのログ
ログを見に行きました。~/Library/Logs/restic/ には実行のたびにログが積まれる作りにしてあります。
| 1 2 3 | backup-20260815-115315.log 274 バイト backup-20260815-114830.log 274 バイト backup-20260814-160941.log 274 バイト |
ジョブは動いていました。動いたうえで失敗していた。3 本とも同じ 274 バイトなのは、同じところで同じように落ちているからです。成功したログは数 KB あります。
中身は 3 行でした。
| 1 2 3 | [2026-08-15 11:53:15] === restic バックアップ開始 === [2026-08-15 11:53:34] エラー: リポジトリを開けません: sftp:user@nas.local:/home/restic/plugins-repo [2026-08-15 11:53:34] 未作成の場合は 'restic init' を手動で実行してください |
理由が書かれていません。自分で書いたラッパースクリプトを読み返して、原因が分かりました。
| 1 | if ! "$RESTIC_BIN" cat config >/dev/null 2>&1; then |
2>&1 で restic のエラー出力を /dev/null に流していたのです。真因を自分の手で捨てていました。
手動で走らせたら、restic はちゃんと理由を教えてくれました。SSH がパスワードを聞いてきます。鍵は ~/.ssh/nas という名前で置いてあったのですが、~/.ssh/config に書いていませんでした。
restic の SFTP バックエンドは素の ssh を起動します。デフォルト名(id_ed25519 など)以外の鍵は、~/.ssh/config に書くか ssh-agent に載せておかないと拾ってくれません。構築した日は agent に載っていて動いていたのが、再起動で消えたのだと思います。ssh-add は --apple-use-keychain を付けないと再起動で揮発するそうなので、そのあたりが噛み合っていなかったのでしょう。
~/.ssh/config に 5 行足したら通りました。launchd から起動されるジョブには端末がないので、パスワードを聞かれた時点で詰みます。
なぜ 2 日間、通知が来なかったのか
スクリプトには失敗時に通知センターへ出す関数を書いてありました。それが鳴っていない。
読み返して、頭を抱えました。
| 1 2 3 4 5 6 7 | if [[ -n "${REQUIRE_MOUNT:-}" ]]; then if ! mount | grep -q " on ${REQUIRE_MOUNT} "; then log "スキップします" warn_if_stale # ← ここにしかない exit 0 fi fi |
「最後に成功してから N 日経ったら警告する」関数の呼び出しが、外付けドライブが未マウントだったときの分岐の中にしかありませんでした。バックアップ先を NAS に移したとき REQUIRE_MOUNT="" にしたので、外側の if が常に偽になります。つまりこの関数は、書いてから一度も実行されていません。
保険を買ったつもりで、契約書を引き出しに入れたまま忘れていたようなものです。
事前チェックの先頭で無条件に呼ぶよう移しました。ただし 30 分間隔にすると、止まっている間ずっと同じ通知が 1 日 48 回飛びます。ログには毎回出しつつ、通知そのものは 1 日 1 回に絞るゲートを入れました。
8/15 12:09、10 分前のスナップショットが消えた
鍵を直してバックアップが通るようになったので、次は間隔と保持ポリシーです。ここで冒頭の図の話が出てきます。
まず旧設定のまま forget を走らせました。11:59 に作ったスナップショットが、12:09 の世代整理で消えました。同じ日の後発(12:08 のもの)に daily 枠を奪われたからです。
10 分前の状態が、10 分後には戻せなくなっている。間隔を 30 分にしても、保持ポリシーを直さなければこれが 1 日中続きます。設定を変えた動機そのものが、目の前で再現された形でした。
--keep-within 2d を足して同じことをやり直すと、12:08 と 12:15 の両方が within 2d の理由で残りました。削除対象はゼロ。
restic の保持ポリシーは、条件ごとに残す枠を持っていて、それらの和集合が生き残ります。--keep-within と --keep-daily は取り合いをしません。短期を厚く、長期を薄く、という組み方ができます。
保持ポリシーをどう組むか
いま使っているのは、直近 2 日を全部、そこから先は日次 7・週次 4・月次 12・年次 3 です。
--keep-within 2d の代わりに --keep-last 96(直近 96 本)や --keep-hourly 48(1 時間ごとに最新 1 本を 48 本)でも似たことができます。--keep-within を選んだのは、あとから実行間隔を変えても勝手に追従するからです。本数で指定すると、30 分を 15 分に変えたときに保持される時間が半分になります。
容量の心配はほぼ要りません。restic はコンテンツ定義チャンクで重複排除するので、変更のないファイルは何度スナップショットを取っても実データが増えません。手元のリポジトリは 11,300 ファイル・238 MiB ですが、同じ内容のスナップショットを 1 本足したときの prune の出力は total prune: 0 blobs / 0 B でした。増えるのはツリーとインデックスのメタデータだけです。
変更したら必ず --dry-run で確かめてください。restic は適用したポリシーを 1 行目に書き出します。
| 1 2 | Applying Policy: keep 7 daily, 4 weekly, 12 monthly, 3 yearly snapshots and all snapshots within 2d of the newest |
and all snapshots within 2d of the newest が出ていなければ、設定が届いていません。実際わたしは、設定ファイルだけ直してスクリプトを差し替え忘れ、この行が出ないまま「直った」と思い込みかけました。
実行間隔を launchd で変える
macOS なら launchd です。plist の StartInterval を秒で指定します。21600 が 6 時間、1800 が 30 分。
多重起動は気にしなくて構いません。launchd は同じラベルのジョブを並行して走らせないそうで、前回が終わっていなければ次は待たされます。手元で 30 分間隔にしてから重複実行は起きていないので、ロックファイルを自前で持つのはやめました。
30 分にしても体感の負荷はありません。11,300 ファイルのスキャンが 1 秒未満、変更がなければ SFTP 越しでも 2 秒で終わります。caffeinate -i でスリープを抑え、LowPriorityIO と Nice=5 を plist に入れてあるので、作業中に気づくこともないです。
ここで 1 つ気をつけたのが、世代整理と整合性チェックの間隔を実行回数で数えていないかという点です。「10 回に 1 回 prune する」という作りにしていると、間隔を 12 分の 1 にした瞬間に prune の頻度が 12 倍になります。うちは最後に成功した時刻をファイルに記録して秒数で比べる作りだったので、間隔を変えても世代整理は 1 日 1 回、チェックは週 1 回のままでした。
余談ですが、SFTP 接続のたびに connection is not using a post-quantum key exchange algorithm という警告が出るようになりました。NAS 側の OpenSSH が古いというだけで、バックアップの動作には関係ありません。
設定ファイル全文
保持ポリシーの部分を抜き出します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # 直近この期間のスナップショットは間引かずに全部残す。 # 空文字 "" にすると無効。30 分間隔なら 2d でおよそ 96 世代。 KEEP_WITHIN="2d" # 各 1 時間につき最新 1 本を、この本数ぶん残す。既定は無効。 KEEP_HOURLY="" # 長期保持 KEEP_DAILY=7 KEEP_WEEKLY=4 KEEP_MONTHLY=12 KEEP_YEARLY=3 # 世代整理は 1 日 1 回、整合性チェックは週 1 回(いずれも秒) FORGET_INTERVAL=86400 CHECK_INTERVAL=604800 CHECK_READ_SUBSET="5%" # 最後に成功したバックアップから何日で警告するか STALE_WARN_DAYS=1 |
ラッパー側は、短期保持が設定されているときだけオプションを足す形にしました。空文字なら従来どおりの挙動になるので、古い設定ファイルのままでも壊れません。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | FORGET_OPTS=(--tag auto) [[ -n "$KEEP_WITHIN" ]] && FORGET_OPTS+=(--keep-within "$KEEP_WITHIN") [[ -n "$KEEP_HOURLY" ]] && FORGET_OPTS+=(--keep-hourly "$KEEP_HOURLY") FORGET_OPTS+=( --keep-daily "$KEEP_DAILY" --keep-weekly "$KEEP_WEEKLY" --keep-monthly "$KEEP_MONTHLY" --keep-yearly "$KEEP_YEARLY" --prune ) log "--- 世代整理 (forget ${FORGET_OPTS[*]}) ---" "$RESTIC_BIN" forget "${FORGET_OPTS[@]}" |
組み立てたオプションをそのままログに出しているのは、さっき書いた「差し替え忘れ」を検知するためです。ログを見れば、どのポリシーで走ったかが一目で分かります。
リポジトリを開けなかったときは、restic の出力を捨てずに残します。
| 1 2 3 4 5 6 7 8 9 | if ! repo_err="$("$RESTIC_BIN" cat config 2>&1 >/dev/null)"; then log "エラー: リポジトリを開けません: ${RESTIC_REPOSITORY}" if [[ -n "$repo_err" ]]; then log " restic の出力:" while IFS= read -r line; do log " ${line}"; done <<< "$repo_err" fi notify "restic バックアップ" "リポジトリを開けません" exit 1 fi |
2>&1 >/dev/null は順番が肝で、先に標準エラーを現在の標準出力へ向けてから、標準出力を捨てます。逆に書くと両方消えます。
SSH の設定はこれだけです。
| 1 2 3 4 5 | Host nas.local HostName nas.local User user IdentityFile ~/.ssh/nas IdentitiesOnly yes |
launchd の間隔変更はコマンドで済みます。
| 1 2 3 4 | PLIST=~/Library/LaunchAgents/works.example.restic-backup.plist /usr/libexec/PlistBuddy -c "Set :StartInterval 1800" "$PLIST" launchctl bootout gui/$(id -u)/works.example.restic-backup launchctl bootstrap gui/$(id -u) "$PLIST" |
バックアップは、組んだ日がいちばん健康です。あとは劣化していくだけで、しかも劣化したことを教えてくれません。今回わたしが直したのは restic の設定というより、壊れたときに自分が気づけるかどうかでした。
もし同じような仕組みを回しているなら、今日いちばん新しいスナップショットの日付だけ見てみてください。1 分で終わります。それが今日の日付でなかったら、そこから先はわたしと同じ道です。

コメント