systemd timerでresticバックアップを安全に自動化する【Ubuntu Server 26.04】

systemd timerが安全確認後にresticを実行し、Dockerバックアップを外付けSSDへ暗号化保存する流れ はじめての自宅サーバー

前回は、Dockerバックアップをresticで暗号化し、外付けSSDへ手動保存しました。今回はUbuntu Server 26.04 LTSのsystemd timerで毎日実行します。

ただし、自動化の目的は無条件に動かすことではありません。SSDが未マウントなら、同名の空ディレクトリが内蔵ディスク上に残り、そこへ誤保存する危険があります。また、前段の.tgzが古いままでもrestic自体は正常終了できます。

前回のresticバックアップ手順を完了済みとして、正しい媒体・新しい元バックアップ・正しいSHA-256を確認できた場合だけ実行する構成にします。

自動化するのは、既存の.tgz.sha256をresticへ二次バックアップする工程です。

Docker volumeから.tgzを作る工程は含みません。最新アーカイブが36時間より古ければ失敗にします。

この記事のゴールと対象外

  • 一般ユーザー権限のsystemd system serviceで実行する
  • SSDのマウント、UUID、書き込み可能状態を検査する
  • 元バックアップの鮮度とSHA-256を検査する
  • 排他ロック付きでrestic backupとcheckを実行する
  • 正常・未マウント・鮮度エラーをテストする
  • journalから成功・失敗を確認する

自動forgetprune、SSDの自動マウント・自動アンマウント、メール通知、DBバックアップ、クラウド同期は対象外です。

書き込み前の4つの停止条件

外付けSSDのマウントポイント、UUID、書き込み可能状態を確認して誤保存を止める安全ゲート
マウントポイント・媒体UUID・書き込み可能状態のすべてが一致した場合だけ先へ進みます。
  1. /mnt/backup-drive自体がマウントポイント
  2. ファイルシステムUUIDが予定したSSDと一致
  3. 最新.tgzが36時間以内
  4. すべての.sha256がOK

ディレクトリの存在だけではマウント済みとは限りません。また別のUSBディスクが同じ場所へマウントされる場合もあるため、UUIDまで照合します。古いアーカイブを毎日再保存して「成功」にしないため、鮮度も検査します。

timer・service・scriptの役割

systemd timer、oneshotサービス、排他ロック付きスクリプト、resticリポジトリの実行順序

timerは時刻、serviceは権限、スクリプトは検証とバックアップを担当します。
  1. timerが実行時刻を管理
  2. oneshot serviceがユーザー、権限、保護を固定
  3. flockが二重実行を拒否
  4. scriptが媒体、鮮度、SHA-256を検査
  5. resticがbackup、snapshots、checkを実行
  6. 出力をsystemd journalへ記録

1. 実行ユーザーとSSDのUUIDを確認する

id
getent passwd "$USER"
MOUNT_POINT='/mnt/backup-drive'

findmnt --mountpoint "$MOUNT_POINT"   --output SOURCE,TARGET,FSTYPE,OPTIONS,UUID
lsblk --output NAME,PATH,TYPE,TRAN,SIZE,MODEL,FSTYPE,UUID,MOUNTPOINTS

ユーザー、グループ、ホーム、UUIDを控えます。UUIDが空、型番や容量が予定と違う場合は先へ進みません。

2. root管理の環境ファイルを作る

sudo \
  ins\
tall -d -m 700 -o root -g root /e\
tc/ouchi-lab
sudo \
  na\
no /e\
tc/ouchi-lab/restic-backup.env

YOUR_USERPUT_UUID_HEREを実機値へ置換します。

BACKUP_MOUNT=/mnt/backup-drive
BACKUP_UUID=PUT_UUID_HERE
SOURCE_DIR=/home/YOUR_USER/docker/nginx-lan/backups
RESTIC_REPOSITORY=/mnt/backup-drive/restic/ouchi-lab
RESTIC_PASSWORD_FILE=/home/YOUR_USER/.config/restic/ouchi-lab-password
RESTIC_CACHE_DIR=/var/cache/ouchi-lab-restic
MAX_SOURCE_AGE_SECONDS=129600

パスワードそのものではなくパスだけを書きます。

sudo \
  ch\
own root:root /e\
tc/ouchi-lab/restic-backup.env
sudo \
  ch\
mod 600 /e\
tc/ouchi-lab/restic-backup.env
sudo \
  st\
at -c '%a %U %G %n' /e\
tc/ouchi-lab/restic-backup.env

期待値は600 root rootです。このファイルをチャットやIssueへ貼り付けません。

3. 安全確認付きスクリプトを配置する

完全版スクリプトは、記事と同じリポジトリのexamplesディレクトリにあるouchi-lab-restic-backup.shです。内容を確認し、後述のserviceが参照する場所へ保存します。

スクリプトは次の順番で処理します。

  1. 必須環境変数の存在確認
  2. マウントポイントとUUIDの照合
  3. realpathでリポジトリ実体がSSD配下か確認
  4. パスワードファイルの通常ファイル・600・所有者確認
  5. .tgzと対応する.sha256の検証
  6. 最新アーカイブの36時間以内という鮮度確認
  7. restic backup後にSSDを再確認し、snapshotsとcheckを実行

保存後はroot所有・755へ固定し、sudo bash -n /usr/local/sbin/ouchi-lab-restic-backupに出力がないことを確認します。スクリプトはforgetprune、削除、unlock、repair、アンマウントを実行しません。

resticの非0終了はすべて失敗として伝播します。終了コード3も不完全なバックアップなので成功扱いせず、SuccessExitStatus=3|| trueを追加しません。

SHA-256確認とrestic読取の間に元ファイルが変わらないよう、.tgz生成処理とは時刻を重ねません。生成側は一時名へ書き、完成後に最終名へ変更し、完成済みファイルを上書きしない運用にします。

4. oneshot serviceを作る

sudo \
  na\
no /e\
tc/systemd/system/ouchi-lab-restic-backup.service

YOUR_USER3か所とYOUR_GROUPを実機値へ置換します。

[Unit]
Description=Ouchi Lab encrypted restic backup
After=local-fs.target
AssertPathIsMountPoint=/mnt/backup-drive
AssertPathIsReadWrite=/mnt/backup-drive

[Service]
Type=oneshot
User=YOUR_USER
Group=YOUR_GROUP
Environment=HOME=/home/YOUR_USER
EnvironmentFile=/e\
tc/ouchi-lab/restic-backup.env
ExecStart=/u\
sr/bin/flock --nonblock --conflict-exit-code=75 /run/ouchi-lab-restic-backup/backup.lock /u\
sr/local/sbin/ouchi-lab-restic-backup

UMask=0077
Nice=10
IOSchedulingClass=idle
RuntimeDirectory=ouchi-lab-restic-backup
RuntimeDirectoryMode=0700
RuntimeDirectoryPreserve=yes
CacheDirectory=ouchi-lab-restic
CacheDirectoryMode=0700

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/mnt/backup-drive
ReadWritePaths=/var/cache/ouchi-lab-restic

TimeoutStartSec=2h
Restart=no

StandardOutput=journal
StandardError=journal
SyslogIdentifier=ouchi-lab-restic-backup

ConditionPathIsMountPoint=の不一致はスキップ扱いですが、AssertPathIsMountPoint=の不一致は開始失敗になります。UUIDはスクリプトで追加確認します。

system serviceですが、処理はUser=で一般ユーザーへ落とします。ホームは読取専用にし、外付けSSDと専用cacheを明示的な書き込み先にします。

5. timerを作る

sudo \
  na\
no /e\
tc/systemd/system/ouchi-lab-restic-backup.timer
[Unit]
Description=Run Ouchi Lab restic backup daily

[Timer]
OnCalen\
dar=daily
Persistent=true
RandomizedDelaySec=15m
AccuracySec=1m
Unit=ouchi-lab-restic-backup.service

[Install]
WantedBy=timers.target

毎日3時30分を基準に0〜15分遅延します。Persistent=trueは停止中に逃した実行を次回起動後に補いますが、SSD未マウントならassertで失敗します。

6. 有効化前に構文と時刻を検証する

sudo systemd-analyze verify   /e\
tc/systemd/system/ouchi-lab-restic-backup.service   /e\
tc/systemd/system/ouchi-lab-restic-backup.timer
systemd-analyze calendar daily
sudo systemctl daemon-reload

unknown directive、実行ファイル不在、構文エラーがなく、次回時刻が意図どおりか確認します。まだtimerは有効化しません。

7. 正常系を手動実行する

手動実行、SSD未マウント、多重起動、鮮度エラーをsystemd journalで確認するテスト

正常系だけでなく、未マウント・ロック競合・古いバックアップでも意図どおり失敗することを確認します。
sudo systemctl start ouchi-lab-restic-backup.service
sudo systemctl status ouchi-lab-restic-backup.service --no-pager
sudo journalctl -u ouchi-lab-restic-backup.service   --since today --no-pager
sudo systemctl show ouchi-lab-restic-backup.service   -p Result -p ExecMainStatus

oneshotは終了後にinactive (dead)でも正常です。Result=successExecMainStatus=0、完了メッセージ、新しいsnapshot時刻を確認します。

8. SSD未マウントで失敗することを確認する

timerが無効なまま、SSDを使う処理がないことを確認して手動アンマウントします。

cd "$HOME"
sync
sudo \
  umo\
unt -- /mnt/backup-drive
findmnt --mountpoint /mnt/backup-drive || true

sudo systemctl start ouchi-lab-restic-backup.service
sudo systemctl status ouchi-lab-restic-backup.service --no-pager
sudo journalctl -u ouchi-lab-restic-backup.service -n 50 --no-pager

開始が非0、statusがfailed、journalにassert失敗が表示されれば安全側です。成功した場合や内蔵ディスク側へファイルが増えた場合はtimerを有効化しません。

既存の環境固有手順でSSDを再マウントし、UUIDを再確認します。デバイス名を推測してマウントしません。

findmnt --mountpoint /mnt/backup-drive   --output SOURCE,TARGET,FSTYPE,OPTIONS,UUID
sudo systemctl reset-failed ouchi-lab-restic-backup.service

9. 鮮度エラーを安全にテストする

実データを変更せず、許容秒数だけ一時的に短くします。

sudo \
  c\
p -p /e\
tc/ouchi-lab/restic-backup.env   /e\
tc/ouchi-lab/restic-backup.env.before-age-test
sudo \
  na\
no /e\
tc/ouchi-lab/restic-backup.env

MAX_SOURCE_AGE_SECONDS=1へ変更し、数秒後に実行します。

sudo systemctl start ouchi-lab-restic-backup.service
sudo systemctl status ouchi-lab-restic-backup.service --no-pager
sudo journalctl -u ouchi-lab-restic-backup.service -n 50 --no-pager

鮮度エラーで失敗し、restic開始メッセージがないことを確認して戻します。

sudo \
  c\
p -p /e\
tc/ouchi-lab/restic-backup.env.before-age-test   /e\
tc/ouchi-lab/restic-backup.env
sudo \
  r\
m /e\
tc/ouchi-lab/restic-backup.env.before-age-test
sudo systemctl reset-failed ouchi-lab-restic-backup.service

削除対象は今作った退避ファイルだけです。復元成功と名前を確認してから削除します。

10. 排他ロック競合をテストする

正常系の実行後、2つの端末を使います。端末Aでserviceと同じロックを60秒保持します。YOUR_USERを置換してください。

sudo -u YOUR_USER flock   /run/ouchi-lab-restic-backup/backup.lock   sleep 60

60秒以内に端末Bでserviceを起動します。

sudo systemctl start ouchi-lab-restic-backup.service
sudo systemctl show ouchi-lab-restic-backup.service   -p Result -p ExecMainStatus
sudo journalctl -u ouchi-lab-restic-backup.service -n 50 --no-pager

Result=exit-codeExecMainStatus=75なら二重実行を拒否できています。端末Aの終了後にfailed状態を解除し、正常系をもう一度確認します。

sudo systemctl reset-failed ouchi-lab-restic-backup.service
sudo systemctl start ouchi-lab-restic-backup.service

11. timerを有効化する

正常系と失敗系を確認できた場合だけ有効化します。

sudo systemctl enable --now ouchi-lab-restic-backup.timer
systemctl status ouchi-lab-restic-backup.timer --no-pager
systemctl list-timers ouchi-lab-restic-backup.timer   --all --no-pager

active (waiting)とNEXTを確認します。timerがwaitingでも直前serviceの成功は保証しないため、翌日は両方を確認します。

systemctl show ouchi-lab-restic-backup.service   -p Result -p ExecMainStatus
sudo journalctl -u ouchi-lab-restic-backup.service   --since=-2days --no-pager
systemctl list-timers ouchi-lab-restic-backup.timer   --all --no-pager

停止・無効化・ロールバック

sudo systemctl disable --now ouchi-lab-restic-backup.timer
systemctl status ouchi-lab-restic-backup.timer --no-pager

これで次回実行を止められます。設定自体も削除する場合は、timerとserviceが停止済みであることを確認します。

sudo systemctl disable --now ouchi-lab-restic-backup.timer
sudo systemctl stop ouchi-lab-restic-backup.service
sudo r\
m</span> /e\
tc/systemd/system/ouchi-lab-restic-backup.timer
sudo r\
m</span> /e\
tc/systemd/system/ouchi-lab-restic-backup.service
sudo r\
m</span> /u\
sr/local/sbin/ouchi-lab-restic-backup
sudo r\
m</span> /e\
tc/ouchi-lab/restic-backup.env
sudo systemctl daemon-reload

resticリポジトリ、snapshot、パスワードファイル、元の.tgzは削除しません。設定削除とバックアップデータ削除を分けます。

トラブルシューティング

status=20〜24

SSDのマウント、UUID、書き込み先が不一致です。設定を緩めず、findmntlsblkで媒体を確認します。

status=35

最新.tgzが古い状態です。timerの時刻を変える前に、Docker volumeからアーカイブを作る前段の運用を確認します。

status=75

排他ロック競合です。別service、cron、手動実行がないか確認します。ロックファイルを削除して回避せず、実行プロセスを特定します。

timerはactiveなのにsnapshotが増えない

timerとserviceは別unitです。list-timersだけでなく、serviceのstatusとjournalを確認します。

完了チェックリスト

  • 実行ユーザー、グループ、ホームを実機値へ置換した
  • SSDのUUIDを型番・容量と一緒に照合した
  • 環境ファイルが600 root root
  • スクリプトが755 root rootsh -nに成功した
  • unitとtimerをsystemd-analyze verifyした
  • 手動serviceが成功した
  • SSD未マウントでfailedになった
  • 古い元バックアップでrestic開始前に止まった
  • timerのNEXTとserviceのResultを別々に確認した
  • 自動prune・自動アンマウントを追加していない

まとめ

安全な自動化は、成功ルートより止まる条件が重要です。timer、一般ユーザーのoneshot service、排他ロック、UUID、鮮度、SHA-256、restic checkを層に分けると、失敗理由をjournalから追えます。

Persistent=RandomizedDelaySec=AccuracySec=Ubuntu 26.04のsystemd.timerマニュアル、assertとconditionの違いはsystemd.unitマニュアル、実行ユーザーと保護設定はsystemd.serviceマニュアルで確認できます。resticの環境変数と終了コードはrestic公式Scripting、排他制御はflockマニュアルを確認しました。確認日は2026年8月4日です。

次は、journalの失敗を見逃さないためのローカル監視と通知を設計します。

コメント

タイトルとURLをコピーしました