前回は、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から成功・失敗を確認する
自動forget・prune、SSDの自動マウント・自動アンマウント、メール通知、DBバックアップ、クラウド同期は対象外です。
書き込み前の4つの停止条件

/mnt/backup-drive自体がマウントポイント- ファイルシステムUUIDが予定したSSDと一致
- 最新
.tgzが36時間以内 - すべての
.sha256がOK
ディレクトリの存在だけではマウント済みとは限りません。また別のUSBディスクが同じ場所へマウントされる場合もあるため、UUIDまで照合します。古いアーカイブを毎日再保存して「成功」にしないため、鮮度も検査します。
timer・service・scriptの役割
- timerが実行時刻を管理
- oneshot serviceがユーザー、権限、保護を固定
flockが二重実行を拒否- scriptが媒体、鮮度、SHA-256を検査
- resticがbackup、snapshots、checkを実行
- 出力を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_USERとPUT_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が参照する場所へ保存します。
スクリプトは次の順番で処理します。
- 必須環境変数の存在確認
- マウントポイントとUUIDの照合
- realpathでリポジトリ実体がSSD配下か確認
- パスワードファイルの通常ファイル・600・所有者確認
- 各
.tgzと対応する.sha256の検証 - 最新アーカイブの36時間以内という鮮度確認
- restic backup後にSSDを再確認し、snapshotsとcheckを実行
保存後はroot所有・755へ固定し、sudo bash -n /usr/local/sbin/ouchi-lab-restic-backupに出力がないことを確認します。スクリプトはforget、prune、削除、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. 正常系を手動実行する
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=success、ExecMainStatus=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-code、ExecMainStatus=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、書き込み先が不一致です。設定を緩めず、findmntとlsblkで媒体を確認します。
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 rootでsh -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の失敗を見逃さないためのローカル監視と通知を設計します。



コメント