WEELWORKS

systemd timer に載せて、毎晩動かす

前回、設定は profiles.yaml に集まりました。ただ実行はまだ手動です。今回は自動実行を載せます。

cron でも動きますが、この連載では systemd timer を使います。理由は3つあります。実行結果が journal に残ること、サーバが止まっていて実行を逃した場合に追いかけて実行できること、そして依存関係(ネットワークやマウントが準備できてから動かす)を宣言で書けることです。連載の後半でリモートの保管先を扱うようになると、3つ目が効いてきます。

schedule を1行足す

profiles.yamlbackupschedule を書きます。

version: 2

global:
  restic-binary: /usr/local/bin/restic

profiles:
  default:
    repository: "/srv/backup/repo"
    password-file: "/root/.config/restic/password"
    lock: "/tmp/resticprofile-default.lock"
    force-inactive-lock: true

    backup:
      source: [/srv/data]
      exclude-caches: true
      verbose: true
      schedule: "*-*-* 02:30:00"
      schedule-permission: system
      schedule-lock-wait: 15m

    retention:
      after-backup: true
      keep-daily: 7
      keep-weekly: 4
      keep-monthly: 6
      prune: true

    check:
      read-data-subset: "5%"
      schedule: "Sun *-*-* 04:00:00"
      schedule-permission: system

新しく出てきたキーは4つです。

  • schedule — 実行時刻。systemd の OnCalendar 書式をそのまま書きます。
  • schedule-permission: system — システム全体の unit として登録します(user にするとユーザー unit になります)。
  • schedule-lock-wait: 15m — 他の処理がロックを持っていたら、最大15分待ってから諦めます。
  • check セクション — 第3回で見た restic check --read-data-subset を、週1回の別スケジュールとして登録します。

check を毎日ではなく日曜だけにしているのは、実データを読み直す処理だからです。毎日5%ずつでも構いませんが、バックアップと同時に走らせるとロックが競合します。時間帯を分けています。

書式を先に確かめる

OnCalendar の書式は間違えやすく、しかも間違えても静かに通ってしまうことがあります。登録する前に systemd-analyze で確認してください。

systemd-analyze calendar "*-*-* 02:30:00"
Normalized form: *-*-* 02:30:00
    Next elapse: Sat 2026-08-15 02:30:00 JST
       (in UTC): Fri 2026-08-14 17:30:00 UTC
       From now: 14h left
systemd-analyze calendar "Sun *-*-* 04:00:00"
Normalized form: Sun *-*-* 04:00:00
    Next elapse: Sun 2026-08-16 04:00:00 JST
       (in UTC): Sat 2026-08-15 19:00:00 UTC
       From now: 1 day 15h left

UTC 換算まで表示してくれるのが有用です。サーバのタイムゾーンが UTC のまま、意図した時刻の9時間ずれで動いていた、という事故はこれで防げます。

登録する

cd /etc/resticprofile
sudo resticprofile schedule --all
Normalized form: *-*-* 02:30:00
    Next elapse: Sat 2026-08-15 02:30:00 JST
2026/08/14 12:14:47 scheduled job default/backup created
Normalized form: Sun *-*-* 04:00:00
    Next elapse: Sun 2026-08-16 04:00:00 JST
2026/08/14 12:14:47 scheduled job default/check created

--all は「設定ファイルにある schedule を全部登録する」という指定です。backupcheck の2つが登録されました。

生成された unit を読む

自動生成されたものを読まずに使うと、後で何が起きているのか分からなくなります。中身を見ます。

ls -1 /etc/systemd/system/resticprofile-*
/etc/systemd/system/[email protected]
/etc/systemd/system/[email protected]
/etc/systemd/system/[email protected]
/etc/systemd/system/[email protected]

service(何をするか)と timer(いつやるか)が対になっています。systemd の timer は常にこの2枚組です。

まず service です。

[Unit]
Description=resticprofile backup for profile default in profiles.yaml

[Service]
Type=notify
WorkingDirectory=/etc/resticprofile
ExecStart=/usr/local/bin/resticprofile --no-prio --no-ansi --config profiles.yaml run-schedule backup@default
Environment="RESTICPROFILE_SCHEDULE_ID=profiles.yaml:backup@default"
Environment="HOME=/root"

読みどころは ExecStart です。resticprofile ... run-schedule backup@default を呼んでいるだけで、そこから先は前回 --dry-run で見た restic のコマンドラインに繋がります。設定ファイル → resticprofile → restic という経路が、どこも隠れていません。

--no-ansi は色付けのエスケープシーケンスを出さない指定です。journal に制御文字が混ざるのを防いでいます。

次に timer です。

[Unit]
Description=backup timer for profile default in profiles.yaml

[Timer]
OnCalendar=*-*-* 02:30:00
Unit[email protected]
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true が入っているのが重要です。 これは「実行時刻にマシンが止まっていた場合、次に起動したときに実行する」という指定です。

cron にはこの機能がありません。夜中の2時半にバックアップを設定して、その時間帯にサーバを落としていれば、その日のバックアップは単に存在しないことになります。しかも誰も気づきません。Persistent=true があれば、起動直後に遅れて実行されます。

登録された timer を一覧します。

systemctl list-timers "resticprofile-*"
NEXT                             LEFT UNIT                                       ACTIVATES
Sat 2026-08-15 02:30:00 JST       14h [email protected] [email protected]
Sun 2026-08-16 04:00:00 JST 1 day 16h [email protected]  [email protected]

2 timers listed.

実際に動かして確認する

timer の時刻を待つ必要はありません。service を直接起動すれば、timer が起動したときと同じことが起きます。

sudo systemctl start [email protected]
systemctl status [email protected]
     Loaded: loaded (/etc/systemd/system/[email protected]; static)
     Active: inactive (dead) since Fri 2026-08-14 11:52:08 JST; 4s ago
   Duration: 1.767s
TriggeredBy: ● [email protected]
    Process: 2445 ExecStart=/usr/local/bin/resticprofile ... (code=exited, status=0/SUCCESS)
   Main PID: 2445 (code=exited, status=0/SUCCESS)
        CPU: 1.460s

Active: inactive (dead) は異常ではありません。バックアップは走り続けるサービスではなく、実行して終わるものだからです。見るべきは status=0/SUCCESSDuration: 1.767s です。

ログは journal に入っています。

journalctl -u [email protected] -n 15
Aug 14 11:52:08 backup-host resticprofile[2483]: to delete:             4 blobs / 1.128 KiB
Aug 14 11:52:08 backup-host resticprofile[2483]: total prune:           4 blobs / 1.128 KiB
Aug 14 11:52:08 backup-host resticprofile[2483]: remaining:            18 blobs / 5.004 MiB
Aug 14 11:52:08 backup-host resticprofile[2483]: rebuilding index
Aug 14 11:52:08 backup-host resticprofile[2483]: [0:00] 100.00%  2 / 2 indexes processed
Aug 14 11:52:08 backup-host resticprofile[2483]: removing 2 old packs
Aug 14 11:52:08 backup-host resticprofile[2483]: done
Aug 14 11:52:08 backup-host systemd[1]: [email protected]: Deactivated successfully.
Aug 14 11:52:08 backup-host systemd[1]: [email protected]: Consumed 1.460s CPU time.

restic の出力がそのまま journal に流れています。ログファイルの置き場所やローテーションを自分で決める必要がありません。cron を使う場合は、この部分をリダイレクトと logrotate で自前に組むことになります。

まとめて状態を見る

resticprofile 側にも状態を確認するコマンドがあります。

resticprofile status
Profile (or Group) default: backup schedule
===========================================
Normalized form: *-*-* 02:30:00
    Next elapse: Sat 2026-08-15 02:30:00 JST
       (in UTC): Fri 2026-08-14 17:30:00 UTC
       From now: 14h left

Recent log (>= warning in the last month)
==========================================
-- No entries --

Systemd timer status
=====================
[email protected] - backup timer for profile default in profiles.yaml
     Loaded: loaded (/etc/systemd/system/[email protected]; enabled; preset: enabled)
     Active: active (waiting) since Fri 2026-08-14 11:51:47 JST; 25s ago
    Trigger: Sat 2026-08-15 02:30:00 JST; 14h left

Recent log (>= warning in the last month) の節が便利です。直近1か月の警告以上のログだけを journal から拾ってきます。ここが -- No entries -- なら、少なくとも警告は出ていないということです。

検証環境での注意 今回の検証はコンテナ上で行っており、ユーザーセッションの D-Bus がありません。そのため resticprofile statuscannot list user units: Failed to connect to bus: No medium found という行を先に出力しました。schedule-permission: system で登録したシステム unit の状態表示には影響しませんでしたが、通常のサーバでは出ないメッセージです。

登録を解除する

設定を作り直すときは、先に解除します。

sudo resticprofile unschedule --all
2026/08/14 12:14:48 scheduled job default/backup removed
2026/08/14 12:14:48 scheduled job default/check removed

unit ファイルも消えます。

ls /etc/systemd/system/resticprofile-*
ls: cannot access '/etc/systemd/system/resticprofile-*': No such file or directory

schedule を書き換えたら unschedule --all してから schedule --all し直す、と覚えておけば安全です。古い unit が残ったまま新しいものが増える、という状態を避けられます。

全体像

profiles.yamlschedule: *-*-* 02:30:00resticprofile schedule.timerOnCalendar / Persistent.serviceExecStart=resticprofile run-schedulerestic backup / forgetjournald生成生成時刻になったら起動標準出力

ここまでの穴

自動実行になりました。ただ、ここには大きな穴が残っています。

毎晩動いているかどうかを、誰も見ていません。

journalctl を毎朝確認する運用は、2週間もすれば形骸化します。第1回に書いた要件の1つ目――「失敗が失敗として見えること」――は、まだ満たせていません。この穴は連載の中で塞ぎます。

次回

その前に、バックアップ対象を広げます。ファイルをコピーするだけでは正しく取れないものがあるからです。次回は PostgreSQL のダンプを restic に直接流し込みます。そこで、restic が終了コード 0 を返しながら壊れたスナップショットを保存する場面に出会います。

この回で使ったツール