
resticとは、ファイルを可変長のチャンクに分割して重複排除・圧縮・暗号化し、ローカルディスクやREST server、S3互換ストレージへ保存するオープンソースのバックアップツールです。ディスク全体やVMイメージを丸ごと取るものではなく、ファイル単位でスナップショットを積み重ね、任意の時点から任意のパスを戻す用途に向きます。執筆時点でUbuntu 26.04のaptに入るのはrestic 0.18.1で、この記事もその版で検証しています。
対象は、Linuxサーバーの/etcやアプリケーションデータを別マシンへ毎晩退避したい情シス・ホームラボ運用者です。検証では、バックアップ元の1台目(Ubuntu 26.04)から、2台目で動かしたrest-server 0.14.0(append-onlyモード)へバックアップし、3回の実行でリポジトリの増え方を測り、別ディレクトリへ戻してdiff -rで一致を確かめ、保持ポリシーの適用と、途中で出たエラーの再現までを扱います。読み終えると、systemdタイマーで毎晩回る2台構成と、その検証手順が手元に残ります。
この記事でわかること
- 2台構成(restic 0.18.1とrest-server 0.14.0)でバックアップを組む手順(設定ファイルは1行ずつ理由つき)
- 72 MiBのディレクトリを3回バックアップしたときのリポジトリ増分の実測値(2回目は0 B、変更後は4.1 MiB)
restore --includeとdiff -r、check --read-dataでリストアできることを証明する方法- append-onlyサーバーで
forgetが403で拒否されるのに終了コードが0になる落とし穴と、サーバー側でpruneする運用 - restic・rsync・Proxmox Backup Serverをどう使い分けるかの判断表
resticの仕組みと今回の検証構成
対象ファイルはContent Defined Chunking(Rabin fingerprint)で切られ、512 KiB未満のファイルは分割せず、それ以上のファイルは512 KiBから8 MiBのチャンクに分けます。チャンクは内容のハッシュで識別されるため、同じ内容のデータは何回バックアップしても1回しか保存されません。ファイルの途中に行が追記されても、境界が内容で決まるので後続のチャンクはずれずに再利用されます。保存前にはリポジトリのパスワードから導いた鍵で暗号化され、リポジトリのフォーマットバージョン2では圧縮も入ります(既定は--compression auto)。
リポジトリはどのバックエンドでも同じディレクトリ構造で、config、data、index、keys、locks、snapshotsの6つからなります。あとでサーバー側からlsした結果を載せますが、ファイル名はすべてハッシュで、中身は暗号化済みなので、サーバーを預かる側は何が入っているか読めません。逆に言えば、パスワードを失うと誰にも復号できません。
検証環境は、Ubuntu 26.04のコンテナ2台を専用ネットワークでつないだものです。nagoyadevlab01がバックアップ元でrestic 0.18.1をaptから入れ、nagoyadevlab02がバックアップ先でrest-server 0.14.0をGitHubのリリースから入れました。この2つのホスト名と、このあとrest-serverに作るユーザー名のryomaは、どれも検証用に付けた名前です。コマンドやパス(/srv/restic/ryomaなど)に出てくる箇所は、自分のホスト名とユーザー名に読み替えてください。バージョンは次の通りです。
grep VERSION= /etc/os-release
VERSION="26.04.1 LTS (Resolute Raccoon)"
restic version
restic 0.18.1 compiled with go1.25.0 on linux/amd64
rest-server --version
rest-server version rest-server 0.14.0 compiled with go1.24.3 on linux/amd64保存先にrest-serverを選んだ理由は、SFTPやNFSと違って「クライアントが削除できない」設定を持てるからです。SSH経由のsftpバックエンドでは、クライアントの鍵を奪った攻撃者がリポジトリをまるごと消せます。rest-serverの--append-onlyは既存データの削除と変更を拒否するので、バックアップ元が侵害されても過去のスナップショットが残ります。この考え方はランサムウェア対策の実践で整理した「バックアップ側から書き戻せない経路を1本持つ」に当たります。
rest-serverをバックアップ先にappend-onlyモードで立てる
rest-serverは単一のGoバイナリで、依存パッケージはありません。執筆時点の最新リリースは0.14.0で、linux_amd64のtar.gzを展開して/usr/local/binへ置くだけです。専用のシステムユーザーrest-serverを作り、データディレクトリ/srv/resticをそのユーザーだけが読み書きできるようにします。認証は.htpasswdで、bcrypt(-B)かSHAしか受け付けないため、htpasswdコマンドの入ったapache2-utilsを入れます。
curl -LO https://github.com/restic/rest-server/releases/download/v0.14.0/rest-server_0.14.0_linux_amd64.tar.gz
tar xzf rest-server_0.14.0_linux_amd64.tar.gz
sudo install -m 755 rest-server_0.14.0_linux_amd64/rest-server /usr/local/bin/rest-server
sudo useradd -r -s /usr/sbin/nologin rest-server
sudo install -d -o rest-server -g rest-server -m 750 /srv/restic
sudo apt install -y apache2-utils
head -c 24 /dev/urandom | base64 > ~/ryoma.pass
sudo htpasswd -B -c -i /srv/restic/.htpasswd ryoma < ~/ryoma.pass
sudo chown rest-server:rest-server /srv/restic/.htpasswd && sudo chmod 640 /srv/restic/.htpasswdパスワードは/dev/urandomから24バイト取ってファイル(例では~/ryoma.pass)に書き、htpasswd -iで標準入力から読ませています。対話プロンプトで打つ方法もありますが、同じ文字列をあとでクライアントの設定ファイルに貼るので、ファイルに残しておいたほうが転記ミスがありません。貼り終えたらこのファイルは消します。.htpasswdをhtpasswd -cで作るとroot所有になるため、サービスユーザーが読めるように所有者と権限を直しています。
最初の動作確認はフォアグラウンドで起動して、起動ログを見ます。--append-onlyで削除と変更を拒否し、--private-reposでユーザーごとに自分の名前のサブディレクトリ(ユーザー名がryomaなら/ryoma/)だけを許可します。--listenの既定は:8000で、明示しておくと後で読み返したときに迷いません。
sudo -u rest-server rest-server --path /srv/restic --listen :8000 --append-only --private-repos
Data directory: /srv/restic
Authentication enabled
Loaded htpasswd file /srv/restic/.htpasswd
Append only mode enabled
Private repositories enabled
Group accessible repos disabled
start server on [::]:8000常駐させるにはsystemdのユニットにします。DynamicUserではなく先ほど作ったユーザーを指定しているのは、あとでサーバー側から同じユーザーでpruneを走らせるためです。
[Unit]
Description=restic REST server (append-only)
After=network-online.target
Wants=network-online.target
[Service]
User=rest-server
Group=rest-server
ExecStart=/usr/local/bin/rest-server --path /srv/restic --listen :8000 --append-only --private-repos
Restart=on-failure
ProtectSystem=strict
ReadWritePaths=/srv/restic
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now rest-server
sudo systemctl status rest-server検証に使ったコンテナではsystemdを起動できないため、この有効化の3行はこの環境では実行していません。代わりに上のフォアグラウンド起動と同じ引数でデーモンを走らせ、以降の検証はすべてそのプロセスに対して行っています。クライアント側からは、認証なしで叩いて401が返ることを確認しておきます。
curl -s -o /dev/null -w '%{http_code}\n' http://nagoyadevlab02:8000/ryoma/
401この構成はHTTPで、Basic認証のユーザー名とパスワードが毎リクエスト平文で流れます。検証は専用の閉じたネットワークで行いましたが、社内LANやインターネット越しに使うなら--tls --tls-cert --tls-keyで証明書を持たせる(自己署名ならクライアント側で--cacertを指定する)か、WireGuardやTailscaleのトンネルの中だけで待ち受けてください。--no-authは認証そのものを外すフラグで、append-onlyの意味がなくなるので使いません。
Docker派なら公式イメージrestic/rest-serverがあり、/dataにボリュームを当て、環境変数OPTIONSに--append-only --private-reposを渡す形になります。ユーザー追加はコンテナ内のcreate_userスクリプトで行えます。今回は、NASや小さなVMに置くことが多く依存が少ないバイナリ直置きを選びました。
クライアント側:/etc/restic/envとラッパーで設定を1か所にまとめる
1台目のバックアップ元(例ではnagoyadevlab01)にはaptからresticを入れます。設定は/etc/restic/に集め、root以外が読めないようにします。/etcやアプリのデータをすべて読めるのはrootだけなので、バックアップはrootで実行し、対話操作もsystemdタイマーも同じ設定ファイルを読む形にします。
sudo apt update && sudo apt install -y restic
sudo install -d -m 700 /etc/restic
sudo sh -c 'head -c 32 /dev/urandom | base64 > /etc/restic/password'
sudo chmod 600 /etc/restic/passwordリポジトリのパスワードは人が覚える必要がないので、32バイトの乱数をbase64にしたものをファイルに置きます。次の/etc/restic/envが接続先と認証情報のすべてです。
RESTIC_REPOSITORY=rest:http://nagoyadevlab02:8000/ryoma
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_REST_USERNAME=ryoma
RESTIC_REST_PASSWORD=(nagoyadevlab02のryoma.passの中身を貼る)
RESTIC_CACHE_DIR=/var/cache/resticRESTIC_REPOSITORY:rest:で始まるURLがREST backendの指定です。--private-reposを付けたので、パスはrest-serverのユーザー名と同じ(例では/ryoma)でないと拒否されます。RESTIC_PASSWORD_FILE:リポジトリを復号するパスワードのファイル。RESTIC_PASSWORDに直接書くとpsや環境変数のダンプに出るので使いません。RESTIC_REST_USERNAME/RESTIC_REST_PASSWORD:rest-serverのBasic認証。URLにrest:http://ユーザー名:パスワード@...と埋め込む書き方もありますが、環境変数に分けておくとURLをログに出しても認証情報が漏れません。RESTIC_CACHE_DIR:indexとsnapshotのローカルキャッシュ。指定しないと実行ユーザーのホーム配下に作られ、rootと一般ユーザーで別々のキャッシュが育ちます。
除外リストは別ファイルにします。今回はアプリのキャッシュディレクトリと、ログローテーションの一時ファイルを外しました。
/srv/app/cache
*.tmp毎回sourceするのは面倒なので、envを読んでからresticを呼ぶ5行のラッパーを/usr/local/sbin/restic-srvに置きます。set -aは「以降に定義した変数をすべてexportする」指示で、これがないと. /etc/restic/envで読んだ値がresticの環境に渡りません。execで置き換えるので、終了コードはresticのものがそのまま返ります。
#!/bin/sh
# /etc/restic/env を読み込んでから restic を実行する薄いラッパー
set -a
. /etc/restic/env
set +a
exec restic "$@"sudo chmod 600 /etc/restic/env
sudo chmod 755 /usr/local/sbin/restic-srv
sudo restic-srv init
created restic repository 150c037296 at rest:http://nagoyadevlab02:8000/ryoma/
Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is
irrecoverably lost.initの警告どおり、パスワードを失うとリポジトリは誰にも復号できません。/etc/restic/passwordはバックアップ対象の/etcに含まれますが、それ自体がそのパスワードで暗号化されているので、リストアには使えません。パスワードマネージャーへの登録と、印刷して金庫に入れる控えの2系統を、initの直後に済ませてください。サーバー側でpruneを回す場合は同じ値がサーバー側のファイル(例では/srv/restic/ryoma.repo-password)にも必要になります(後述)。
バックアップを3回実行して重複排除を実測する
対象は/etcと、アプリケーション用に用意した/srv/appです。/srv/appはdu -shで72Mあり、中身は乱数で作った画像ファイル、SQLダンプ風のテキスト、アクセスログ風のテキスト、除外対象のcacheディレクトリとrotate.tmpです。乱数データは圧縮も重複排除も効かず、テキストは圧縮が効くという、実際のサーバーに近い混ぜ方にしています。
sudo du -sh /srv/app
72M /srv/app
sudo restic-srv backup /etc /srv/app --exclude-file /etc/restic/excludes --tag daily
no parent snapshot found, will read all files
Files: 156 new, 0 changed, 0 unmodified
Dirs: 70 new, 0 changed, 0 unmodified
Added to the repository: 63.407 MiB (48.855 MiB stored)
processed 156 files, 63.244 MiB in 0:00
snapshot 50faf0d9 saved「Added to the repository: 63.407 MiB (48.855 MiB stored)」の2つの数字が要点です。前者が重複排除後にリポジトリへ送った量、後者が圧縮後に実際に保存した量で、テキスト部分が縮んだ分だけ差が出ています。処理した63.244 MiBがduの72Mより小さいのは、excludesでcacheと*.tmpが外れたためです。サーバー側でduすると50Mでした。
sudo du -sh /srv/restic/ryoma
50M /srv/restic/ryoma何も変えずにもう一度実行すると、親スナップショットとの比較で156ファイルすべてがunmodifiedと判定され、追加は0 Bです。変更の有無はファイルのメタデータ(mtime、ctime、サイズ、inode番号)で判定されるので、2回目はファイルの中身を読まずに終わります。
sudo restic-srv backup /etc /srv/app --exclude-file /etc/restic/excludes --tag daily
using parent snapshot 50faf0d9
Files: 0 new, 0 changed, 156 unmodified
Dirs: 0 new, 0 changed, 70 unmodified
Added to the repository: 0 B (0 B stored)
processed 156 files, 63.244 MiB in 0:00
snapshot 9d630b00 saved3回目の前に、ログの末尾に行を追記し、設定ファイルの1行を書き換え、画像ファイルを1つ足しました。送られた6.530 MiB(保存後 4.112 MiB)の中身は、新しい画像と、追記後のログです。後述のdiffが「Data Blobs: 5 new, 2 removed」「Removed: 1.686 MiB」と出しているとおり、この検証のログは1つのチャンクに収まる小ささだったため、追記分だけでなくファイル全体が新しいblobとして送り直され、古いログと設定ファイルのblobが捨てられています。チャンク境界がファイルの途中に入る大きさのログなら、通常は末尾のチャンクだけが送り直され、先頭側はそのまま再利用されます。
sudo restic-srv backup /etc /srv/app --exclude-file /etc/restic/excludes --tag daily
using parent snapshot 9d630b00
Files: 1 new, 2 changed, 154 unmodified
Dirs: 0 new, 5 changed, 65 unmodified
Added to the repository: 6.530 MiB (4.112 MiB stored)
processed 157 files, 68.088 MiB in 0:00
snapshot a6fee06d saved| 実行 | 変更内容 | Added to the repository | サーバー側 du -sh |
|---|---|---|---|
| 1回目 | 初回(156ファイル、63.244 MiB) | 63.407 MiB(保存 48.855 MiB) | 50M |
| 2回目 | 変更なし | 0 B | 50M |
| 3回目 | ログ追記、設定変更、画像ファイル1つの追加 | 6.530 MiB(保存 4.112 MiB) | 55M |
3世代を保持した状態のstatsを2つのモードで見ると、重複排除の効き方が数字で出ます。既定のrestore-sizeは「全スナップショットを全部戻したときの大きさ」で194.576 MiB、raw-dataは「リポジトリに実際に置かれているblobの合計」で52.958 MiBでした。圧縮前の合計を示すTotal Uncompressed Size 69.944 MiBを1世代分の63.244 MiBと比べると、3世代を持っても6.7 MiBしか増えていません。
sudo restic-srv stats --mode raw-data
scanning...
Stats in raw-data mode:
Snapshots processed: 3
Total Blob Count: 244
Total Uncompressed Size: 69.944 MiB
Total Size: 52.958 MiB
Compression Progress: 100.00%
Compression Ratio: 1.32x
Compression Space Saving: 24.29%世代間で何が変わったかはdiffで追えます。Mが変更、+が追加で、設定ファイルとログの2つが変更、画像1つが追加という操作どおりの結果が返っています。監査で「いつ誰が設定を変えたか」を追う場面では、この出力をそのまま証跡にできます。
sudo restic-srv diff 50faf0d9 a6fee06d
comparing snapshot 50faf0d9 to a6fee06d:
M /srv/app/config/app.conf
M /srv/app/logs/app.log
+ /srv/app/uploads/img-13.bin
Files: 1 new, 0 removed, 2 changed
Dirs: 0 new, 0 removed
Others: 0 new, 0 removed
Data Blobs: 5 new, 2 removed
Tree Blobs: 6 new, 6 removed
Added: 6.530 MiB
Removed: 1.686 MiBスナップショットの確認とリストア
snapshotsはID、時刻、ホスト名、タグ、パスを一覧します。Host列にバックアップ元のホスト名(例ではnagoyadevlab01)が入るので、複数台が同じリポジトリに書く構成でも--hostで絞り込めます。--tag dailyを付けたのは、あとで週次や手動のスナップショットを区別してforgetのポリシーを分けるためです。
sudo restic-srv snapshots --compact
ID Time Host Tags Size
-----------------------------------------------------------------
50faf0d9 2026-09-26 00:26:04 nagoyadevlab01 daily 63.244 MiB
9d630b00 2026-09-26 00:26:06 nagoyadevlab01 daily 63.244 MiB
a6fee06d 2026-09-26 00:26:07 nagoyadevlab01 daily 68.088 MiB
-----------------------------------------------------------------
3 snapshotsリストアは、いきなり本番のパスへ戻さず、--targetで空のディレクトリへ展開してから比較するのが手順の基本です。--include /srv/appで/etcを除いた部分だけを戻し、元ディレクトリとdiff -rで突き合わせます。excludesで外したcacheと*.tmpはリストア先に存在しないので、diff側でも-xで同じものを外して比べています。
sudo restic-srv restore latest --target /tmp/restore-test --include /srv/app
restoring snapshot a6fee06d of [/etc /srv/app] at 2026-09-26 00:26:07.351374503 +0000 UTC by root@nagoyadevlab01 to /tmp/restore-test
Summary: Restored 22 / 21 files/dirs (67.665 MiB / 67.665 MiB) in 0:00
sudo diff -r -x cache -x '*.tmp' /srv/app /tmp/restore-test/srv/app && echo "restore OK"
restore OK「restore OK」が出て初めて、そのスナップショットは戻せると言えます。本番へ反映するときは、サービスを止めてから/tmp/restore-test/srv/app/をrsync -aで本来の場所へ上書きし、所有者と権限が元どおりかls -lで見てから起動します。特定のファイルだけ欲しいときは--includeにそのパスを書けば、そのファイルだけが展開されます。
保持ポリシー:forgetはdry-runで確認し、pruneはサーバー側で回す
世代を減らす操作はforget(スナップショットの登録を消す)とprune(どのスナップショットからも参照されなくなったデータを消す)の2段階です。ポリシーは日次7、週次4、月次6を出発点にしました。--keep-daily 7は「スナップショットが存在する直近7日について、各日の最新1つを残す」という意味なので、同じ日に3回取った今回の検証では、最新のa6fee06dだけが日次として残り、最古の50faf0d9は週次・月次の最古として残り、間の9d630b00が削除対象になります。まず--dry-runで何が消えるかを見ます。
sudo restic-srv forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --dry-run
Applying Policy: keep 7 daily, 4 weekly, 6 monthly snapshots
keep 2 snapshots:
ID Time Host Tags Reasons Paths Size
--------------------------------------------------------------------------------------------------------
50faf0d9 2026-09-26 00:26:04 nagoyadevlab01 daily oldest daily snapshot /etc 63.244 MiB
oldest weekly snapshot /srv/app
oldest monthly snapshot
a6fee06d 2026-09-26 00:26:07 nagoyadevlab01 daily daily snapshot /etc 68.088 MiB
weekly snapshot /srv/app
monthly snapshot
--------------------------------------------------------------------------------------------------------
2 snapshots
remove 1 snapshots:
ID Time Host Tags Paths Size
-------------------------------------------------------------------------------
9d630b00 2026-09-26 00:26:06 nagoyadevlab01 daily /etc 63.244 MiB
/srv/app
-------------------------------------------------------------------------------
1 snapshots
Would have removed the following snapshots:
{9d630b00}ところが、このリポジトリはappend-onlyのrest-serverにあるので、クライアントからの削除は通りません。--dry-runを外して実行すると、スナップショットファイルの削除で403 Forbiddenが返り、リポジトリには3世代が残ったままでした。これは設計どおりの動きです。侵害されたクライアントに世代を消させないために、そもそも削除できない口にしているからです。
sudo restic-srv forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6; echo "exit=$?"
[0:00] 0.00% 0 / 1 files deleted
Remove(<snapshot/9d630b008b>) failed: unexpected HTTP response (403): 403 Forbidden
unable to remove snapshot/9d630b008bd8e9ff204f6921675f65d4031131fdf07ac891e6c7b9ba4bfc694d from the repository
exit=0検証したrestic 0.18.1では、この403のあとも終了コードが0でした(上のexit=0)。forget --pruneと書いた場合も、削除に失敗したあとpruneが「0 blobs」で走ってdoneと出ます。cronやsystemdの成功・失敗だけを監視していると、世代が一切減っていないことに気づけません。append-only構成では、クライアント側にforgetを置かず、次のようにサーバー側で回してください。
サーバー側のpruneは、rest-serverを経由せずローカルパス(例では/srv/restic/ryoma)をリポジトリとして開きます。実行ユーザーをrest-serverに揃えるのは、pruneが新しいindexやpackを書くため、rootで実行するとroot所有のファイルが混ざり、以後rest-serverが削除できなくなるからです。リポジトリのパスワードは/srv/restic/ryoma.repo-password(rest-server所有、600)に置きました。このユーザーにはホームディレクトリがないため、キャッシュ作成に失敗する警告が出ますが、--no-cacheで消えます。検証時点で4世代あったため、削除対象は2つになっています。
サーバー側でresticを直接動かすので、バックアップ先(例ではnagoyadevlab02)にもresticを入れ、クライアントの/etc/restic/passwordと同じ1行をサーバー側のパスワードファイル(例では/srv/restic/ryoma.repo-password)に書きます。下の値は検証環境で使った使い捨てのもので、自分の環境ではクライアント側のファイルの中身に置き換えてください。
sudo apt install -y restic
sudo tee /srv/restic/ryoma.repo-password >/dev/null <<'EOF'
egdAi6Sw4/g7B+Bv32Ch5vG04rRm2c8Ez9ZpJFQ2o0E=
EOF
sudo chown rest-server:rest-server /srv/restic/ryoma.repo-password && sudo chmod 600 /srv/restic/ryoma.repo-password && sudo ls -l /srv/restic/ryoma.repo-password
-rw------- 1 rest-server rest-server 45 Sep 26 00:53 /srv/restic/ryoma.repo-passwordsudo -u rest-server restic -r /srv/restic/ryoma --password-file /srv/restic/ryoma.repo-password forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
unable to open cache: mkdir /home/rest-server: permission denied
Applying Policy: keep 7 daily, 4 weekly, 6 monthly snapshots
[0:00] 100.00% 2 / 2 files deleted
2 snapshots have been removed, running prune
warning: running prune without a cache, this may be very slow!
to repack: 6 blobs / 3.439 KiB
this removes: 4 blobs / 2.593 KiB
to delete: 0 blobs / 0 B
total prune: 4 blobs / 2.593 KiB
remaining: 264 blobs / 76.960 MiB
unused size after prune: 0 B (0.00% of remaining size)
done
sudo -u rest-server restic -r /srv/restic/ryoma --password-file /srv/restic/ryoma.repo-password --no-cache snapshots --compact
ID Time Host Tags Size
-----------------------------------------------------------------
50faf0d9 2026-09-26 00:26:04 nagoyadevlab01 daily 63.244 MiB
2bb83101 2026-09-26 00:27:48 nagoyadevlab01 daily 92.088 MiB
-----------------------------------------------------------------
2 snapshotsサーバーにリポジトリのパスワードを置くことには賛否があります。置けばサーバーの管理者がバックアップの中身を読めるようになり、置かなければpruneのたびに管理端末から作業する必要があります。自分で両方を管理するホームラボや少人数の情シスなら置いてよいと考えますが、バックアップ先を他部署や外部に預ける場合は置かず、管理端末からSSHポートフォワードで到達する非append-onlyの第2インスタンス(--listen 127.0.0.1:8001)に対してforgetとpruneを流す構成にしてください。
保持の指定そのものにも、append-only構成ならではの注意があります。resticのforgetのドキュメントは、侵害されたクライアントが新しい日時の偽スナップショットを追加できる構成では、--keep-dailyのような暦ベースの指定より--keep-within(指定した期間内のスナップショットをすべて残す)を勧めています。暦ベースだと、偽の「最新」スナップショットが各日・各週の枠を埋め、本物が保持対象から押し出されるからです。この記事の構成は攻撃者が削除できないことを前提にしていますが、追加はできる点を忘れずに、サーバー側のポリシーは--keep-within 30dのように期間で指定するか、暦ベースと組み合わせてください(執筆時点のドキュメントの推奨です)。
systemdタイマーで毎晩バックアップする
cronでもよいのですが、systemdタイマーならPersistent=trueで停止中に過ぎた予定を起動後に消化でき、journalctl -u restic-backupで出力がそのまま残ります。serviceはType=oneshotで、バックアップのあとにcheck --read-data-subset 5%を続けて走らせ、毎晩リポジトリの5%をランダムに読み戻して壊れていないかを確かめます。ユニットファイルでは%が特殊文字なので5%%と書きます。
[Unit]
Description=restic backup of /etc and /srv/app to nagoyadevlab02
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-srv backup /etc /srv/app --exclude-file /etc/restic/excludes --tag daily
ExecStartPost=/usr/local/sbin/restic-srv check --read-data-subset 5%%
Nice=10
IOSchedulingClass=idle[Unit]
Description=Nightly restic backup
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.targetNice=10とIOSchedulingClass=idleは、夜間でもアプリの応答を優先するための指定で、バッチが動かない時間帯なら外して構いません。RandomizedDelaySec=15mは、同じrest-serverに複数台が2:30ちょうどに殺到するのを避けるためです。ユニットの文法と時刻指定はsystemd-analyzeで確認できます。
sudo systemd-analyze verify /etc/systemd/system/restic-backup.service /etc/systemd/system/restic-backup.timer; echo "exit=$?"
exit=0
systemd-analyze calendar '*-*-* 02:30:00'
Normalized form: *-*-* 02:30:00
Next elapse: Sat 2026-09-26 02:30:00 UTC
From now: 2h 0min leftsudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timerタイマーの有効化と実際の深夜起動は、systemdが動かない検証コンテナのため実行していません。ユニットの検証(exit=0)と時刻指定の解釈までを確認し、バックアップとcheckの各コマンドはこの記事の各節で個別に実行した結果を載せています。実機では有効化のあとsudo systemctl start restic-backup.serviceで一度手動起動し、journalctl -u restic-backup -n 30に「snapshot ... saved」と「no errors were found」が並ぶことを見てから放置に移ってください。
動作確認
「バックアップが取れている」の判定は、check --read-dataがエラーなしで終わることと、上で行ったリストアのdiff -rが一致することの2つで行います。checkだけだとindexとpackの整合性は見えても、データそのものが読み戻せるかまでは分かりません。--read-dataは全packを取得して復号し、ハッシュを検証するので、所要時間はリポジトリの大きさとネットワークに比例します。今回の53 MiBでは1秒未満でしたが、数百GBなら夜間バッチに収まるよう--read-data-subsetで分割してください。下の出力は、保持ポリシーの節でサーバー側のforget --pruneを回す前(3世代、6 pack)に取ったものです。pruneのあとに同じコマンドを実行すると2 / 2 snapshotsと表示されます。
sudo restic-srv check --read-data
using temporary cache in /var/cache/restic/restic-check-cache-2533883433
create exclusive lock for repository
load indexes
check all packs
check snapshots, trees and blobs
[0:00] 100.00% 3 / 3 snapshots
read all data
[0:00] 100.00% 6 / 6 packs
no errors were found
sudo restic-srv check --read-data-subset 5%
using temporary cache in /var/cache/restic/restic-check-cache-1711564080
create exclusive lock for repository
load indexes
check all packs
check snapshots, trees and blobs
[0:00] 100.00% 3 / 3 snapshots
read 5.0% of data packs
[0:00] 100.00% 1 / 1 packs
no errors were foundサーバー側では、リポジトリの6ディレクトリがそろっていることと、バックアップ完了後にlocksが空になっていることを見ます。ロックファイルの削除だけはappend-onlyモードでも許可されているので、正常終了したあとにロックが残っていれば、どこかのクライアントで処理が途中で落ちた証拠です。
sudo ls /srv/restic/ryoma
config
data
index
keys
locks
snapshots
sudo ls -la /srv/restic/ryoma/locks
total 8
drwx------ 2 rest-server rest-server 4096 Sep 26 00:27 .
drwx------ 7 rest-server rest-server 4096 Sep 26 00:25 ..つまずきポイント
Fatal: Please specify repository location
ラッパーを通さずにresticを直接叩くと、接続先が渡っていないので即座に止まります。この記事の構成では一般ユーザーは/etc/restic/envを読めないので、sudo restic-srvに統一します。同じ理由で、cronにrestic backup ...と素で書いても動きません。
restic snapshots
Fatal: Please specify repository location (-r or --repository-file)Fatal: wrong password or no key found
リポジトリのパスワードが違うときのメッセージで、終了コードは12でした。別のパスワードファイルを渡してしまった例を再現しています。ファイルを新しく作り直したり、末尾の改行の有無が変わったりしても同じエラーになります。init直後に控えた値と/etc/restic/passwordの中身をsha256sumで比べると、どちらが正しいかを画面に出さずに確認できます。
sudo restic-srv snapshots --password-file /etc/hostname
Fatal: wrong password or no key foundunexpected HTTP response (401): 401 Unauthorized
401はrest-serverのBasic認証に落ちたときで、リポジトリのパスワードとは無関係です。URLに認証情報を埋め込む形で、わざと間違えたパスワードを渡した例です。.htpasswdのユーザー名、RESTIC_REST_PASSWORDの値、そして--private-reposを使っているならURLのパスがユーザー名と一致しているか、の3点を順に見ます。403が返る場合は認証は通っていて、append-onlyで拒否された操作(削除・上書き)をしています。
sudo restic -r rest:http://ryoma:wrong@nagoyadevlab02:8000/ryoma --password-file /etc/restic/password snapshots
Stat(<config/>) failed: unexpected HTTP response (401): 401 Unauthorized
Fatal: unable to open config file: unexpected HTTP response (401): 401 Unauthorized
Is there a repository at the following location?
rest:http://ryoma:***@nagoyadevlab02:8000/ryoma/repository is already locked by PID ...
checkやpruneは排他ロックを取るので、バックアップが走っている最中に実行すると次のように止まります。再現のため、大きめのファイルを足して--limit-upload 2048で転送を遅くしたバックアップを裏で走らせ、その間にcheckを実行しました。
sudo restic-srv check
using temporary cache in /var/cache/restic/restic-check-cache-2491785559
create exclusive lock for repository
repo already locked, waiting up to 0s for the lock
unable to create lock in backend: repository is already locked by PID 3521 on nagoyadevlab01 by root (UID 0, GID 0)
lock was created at 2026-09-26 00:27:49 (2.176455103s ago)
storage ID 1539f815
the `unlock` command can be used to remove stale locksメッセージにあるPIDとホスト名を見て、そのプロセスがまだ生きているなら待つだけです。30分より古いロックと、同じマシン上で既に死んだプロセスのロックは自動で古いものとみなされますが、別ホストの生きているプロセスのロックは自分では判断できません。sudo restic-srv unlockは、そのPIDが確かに存在しないと確認できたときだけ実行してください。走っているpruneのロックを外して別のバックアップを書き込むと、リポジトリが壊れます。今回はバックアップの完了後にunlockを実行し、続けてcheckが「no errors were found」で終わることを確認しました。
使い分け:restic・rsync・Proxmox Backup Server
「サーバーのバックアップ」と一口に言っても、守りたい単位で道具が変わります。ファイル単位で世代を持ちたいならrestic、単に別ディスクへ同じツリーを作りたいならrsync、Proxmox VE上のVMやコンテナを丸ごと戻したいならProxmox Backup Server(PBS)です。比較の軸を具体的にすると次のようになります。
| 観点 | restic + rest-server | rsync | Proxmox Backup Server |
|---|---|---|---|
| バックアップの単位 | ファイル・ディレクトリ | ファイル・ディレクトリ | VM・CT・ホスト(PBSクライアント) |
| 世代管理 | スナップショット+forgetのポリシー | なし(--link-destで自作) | 組み込み(prune設定) |
| 重複排除 | チャンク単位、世代をまたいで効く | なし(差分転送のみ) | チャンク単位 |
| 暗号化 | 常に暗号化、鍵はクライアント側 | なし(転送はsshで保護) | クライアント側暗号化を選択可 |
| バックアップ元が侵害されたとき | append-onlyなら過去世代は消せない | コピー先も消される | 権限設計しだい |
| 戻し方 | restoreで任意パスへ | コピー先をそのまま使う | VM単位、ファイル単位の両方 |
| 向く場面 | 複数のLinuxサーバーの/etcとデータ、クラウドストレージへの退避 | NAS内の同期、ステージング環境の複製 | Proxmox VEクラスタ全体 |
判断の目安は3つです。第一に、戻す単位がVMならPBSで、restic側で頑張る理由はありません。VMの中のファイル単位の復元もPBSでできます。手順はProxmox Backup Serverで3-2-1ルールにまとめています。第二に、物理サーバーやクラウドVM、Proxmox以外のハイパーバイザーで動くLinuxが混在しているなら、どこでも同じ手順で動くresticが管理しやすい選択です。第三に、rsyncは「バックアップ」ではなく「複製」と割り切ってください。誤削除やランサムウェアの暗号化は次の同期でコピー先にも伝わります。--link-destで世代を作ることはできますが、それを自作するくらいならresticのforgetポリシーを使うほうが検証も楽です。バックアップ先のマシンとしては、TrueNAS SCALEのようなNASでrest-serverを動かし、その下でZFSスナップショットも取っておくと、rest-server自体の事故にも備えられます。
よくある質問
リストアとはIT用語で何ですか?
保存しておいたデータを、元の場所または指定した場所へ書き戻す操作です。この操作に当たるのがrestoreコマンドで、スナップショットIDかlatestを指定し、--targetで展開先を決めます。取ったことよりも戻せることに意味があるので、定期的にdiff -rまで含めた復元試験を行い、その記録を残すところまでがリストアの運用です。
バックアップをリストアできないのはなぜ?
原因として多いのは、パスワードの控えが違う(wrong password or no key found)、リポジトリの認証情報が変わった(401)、そして戻したいファイルが除外パターンに当たっていて最初から入っていない、の3つです。3つ目はrestic ls latestで対象パスが一覧に出るかを見れば分かります。さらに、途中で落ちたスナップショットは保存されないので、snapshotsに無い時点のデータは戻せません。
resticとKopiaの違いは?
どちらも重複排除と暗号化を備えたスナップショット型のツールで、守れるものに大きな差はありません。設計の違いとして、resticはリポジトリのパスワードで復号鍵を守る単一のCLIで、保持ポリシーはコマンドの引数で都度与えます。Kopiaはリポジトリ側にポリシーを保存し、GUIとサーバーモードを持ちます。この記事のようにrest-serverのappend-onlyで守りたい、systemdタイマーとシェルで完結させたい、という要件ならresticのほうが構成要素が少なく済みます。
resticのスナップショットはどう一覧・管理しますか?
一覧はsnapshotsで、--host、--tag、--pathで絞り込み、--compactで1行表示にできます。中身はls スナップショットID パス、世代間の差分はdiff ID1 ID2です。整理はforget --keep-daily 7 --keep-weekly 4 --keep-monthly 6のようなポリシーを--dry-runで確認してから適用し、容量を実際に減らすには続けてpruneが必要です。この2つは、append-only構成ではサーバー側で実行します。
まとめ
検証した2台構成(restic 0.18.1とrest-server 0.14.0)では、72 MiBのディレクトリを3世代バックアップしても保存量は約53 MiBに収まり、変更なしの回は0 B、ログ追記と画像1枚の追加でも4 MiB強しか増えませんでした。append-onlyのrest-serverはクライアントからの削除を403で拒み、forgetとpruneはサーバー側のrest-serverユーザーで回す運用にすると、バックアップ元が侵害されても世代が守られます。restore --includeとdiff -r、check --read-dataまでを毎回の手順に含めれば、「取ったつもり」で終わりません。まずは/etcだけを対象にrestic-srv initからdiff -rまでを一度通し、systemdタイマーの初回起動をjournalで確認するところから始めてください。