homelab notes ポート開放できない家の自宅サーバー

restic をコンテナで動かして sudo なしでバックアップする

Docker ボリュームの中身は root 所有なので、普通にやるとバックアップに sudo が要る。 cron に sudo を書きたくない。restic 自体をコンテナで動かせば全部解決する。

考え方

restic をホストにインストールせず、公式イメージを都度起動する。 コンテナの中は root なので、マウントしたボリュームの中身をそのまま読める。 ホスト側で必要な権限は docker グループだけ

R() {
  docker run --rm \
    -v "$REPO":/repo \
    -v "$PWFILE":/pw:ro \
    -v ~/homelab:/src/homelab:ro \
    -v monitoring_grafana_data:/src/vol/grafana:ro \
    -v portainer_data:/src/vol/portainer:ro \
    restic/restic:latest -r /repo --password-file /pw "$@"
}

R backup --host myhost --tag homelab --exclude-caches /src/homelab /src/vol
R forget --host myhost --tag homelab --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

ソースは全部 :ro で渡す。バックアップ処理が元データを壊す事故が原理的に起きない。

SQLite は生ファイルをコピーしない

ダッシュボードや設定が SQLite に入っているサービスは多い。 稼働中の .sqlite をそのままコピーすると、書き込み途中の状態を掴んで 壊れたファイルがバックアップされることがある。WAL がある場合はさらに厄介。

先に整合コピーを作り、生ファイルのほうは除外する。

# 整合コピーを staging に作る
docker run --rm -v ~/app/appdata:/a:ro -v "$STAGING":/out \
  alpine:latest sh -c '
    apk add --no-cache sqlite >/dev/null 2>&1
    sqlite3 /a/db/db.sqlite ".backup /out/app-db.sqlite"'

# 生ファイルは除外し、staging のほうを取り込む
R backup --exclude '/src/app/appdata/db/db.sqlite*' ...

復元テストでは PRAGMA integrity_checkok を返すことまで確認する。 ファイルが戻ってきただけでは検証にならない。

何を入れて、何を入れないか

容量の大きいものほど「本当に必要か」を考える価値がある。うちの場合、

対象容量判断
スタック定義(compose ファイル群)数百KB入れる。消えると構成を失う
VPN のノード鍵数KB入れる。再登録が面倒
画像生成のワークフロー・出力65MB入れる。手で作ったので再現できない
画像生成のモデル29GB入れない。再ダウンロードできる
Python の仮想環境・パッケージキャッシュ19GB入れない。再構築できる
LLM のモデル13.6GB入れない。再取得できる

49GB のディレクトリのうち、本当に失うと困るのは 65MB だった。 「再取得できるか」で切ると、バックアップは驚くほど小さくなる。

外付けに移すときの落とし穴

最初は内蔵 NVMe にリポジトリを置いていた。誤削除からは守れるが、ディスク故障では両方消える。 外付け SSD を挿して移すとき、REPO= を1行書き換えるだけでは危ない。

SSD を挿し忘れたまま cron が走ると、Docker がマウント先のパスを ホスト側に自動生成して、空のリポジトリにバックアップし始める。 エラーも出ない。「バックアップできているつもり」が一番怖い。

なので、リポジトリを探す処理を挟んでから動かす。

find_repo() {
  local d
  for d in /mnt/backup /media/$USER/PortableSSD; do
    # マウント済み かつ restic の config が実在すること
    if mountpoint -q "$d" && [ -f "$d/backups/restic/config" ]; then
      echo "$d/backups/restic"; return 0
    fi
  done
  echo "エラー: バックアップ用 SSD が見つかりません" >&2
  return 1
}
REPO=$(find_repo)     # set -e なので、見つからなければここで止まる

ディレクトリの存在ではなく mountpoint -q と中身のファイルの両方を見るのが要点。 ディレクトリは Docker が勝手に作ってしまうので、存在確認だけでは検出できない。

exfat に置くときの豆知識

持ち運ぶ外付けは exfat のことが多い。restic のリポジトリは平坦なファイルの集まりなので exfat でも問題なく動くが、クラスタサイズが 1MB あるとディレクトリだけで 約 290MB のオーバーヘッドが出る(restic は data/00ff の 256 ディレクトリを作るため)。TB 級のディスクなら誤差。

それと、コンテナで動かした restic が書いたファイルは root 所有の 0600 になる。 リポジトリを別の場所にコピーするときは、ホストの cp ではなく コンテナ経由でやらないと「許可がありません」で一部だけコピーされる。 黙って欠けるので気づきにくい。ファイル数を突き合わせて確認したほうがいい。

検証

取れているつもりで取れていない、を避けるために。

restic check --read-data     # 全パックを実読み出しして破損を検出
./restore.sh homarr /tmp/t   # 空ディレクトリにだけ復元させる(上書き事故の防止)

復元先を空ディレクトリに限定しておくと、うっかり本番に展開する事故が防げる。