/var/www/yatta47.log

/var/www/yatta47.log

やったのログ置場です。スクラップみたいな短編が多いかと。

systemd user timer の使い方 — root不要で個人の定期タスクを管理する

systemd のタイマーって、ずっと root 権限のやつしか使ってなかったんですよね。 /etc/systemd/system/ にファイル置いて、sudo systemctl enable して、みたいな流れ。

それが、Claude Code に個人の自動化タスクをどう管理してるか聞いたときに「user timer 使えますよ」って言われて、初めて知りました。 そういう世界があったのか、と。

systemd には 2 つのインスタンスがある

system インスタンスと user インスタンス、完全に別物として動いています。

system インスタンスは /etc/systemd/system/ に設定ファイルを置いて、sudo systemctl で操作するやつ。 user インスタンスは ~/.config/systemd/user/ に置いて、systemctl --user で操作します。

systemctl --user list-timers を実行すると、systemctl list-timers とは全然違うタイマー一覧が出てきます。 同じコマンドのように見えて、管理対象がまったく別なんですよね。

user インスタンスの利点は、sudo が不要なことと、自分のホームディレクトリのパスが使いやすいこと。 個人の自動化タスクには user timer のほうが自然にはまります。

セットアップ手順

設定ファイルは 2 つ作ります。タイマーファイルとサービスファイルです。

まずサービスファイル(何を実行するか):

# ~/.config/systemd/user/my-task.service
[Unit]
Description=My personal automation task

[Service]
Type=oneshot
ExecStart=%h/repos/github/my-project/scripts/run-task.sh

%h はホームディレクトリに展開されます。 Type=oneshot は、コマンドを 1 回実行して終わるタイプ。定期タスクに向いています。

次にタイマーファイル(いつ実行するか):

# ~/.config/systemd/user/my-task.timer
[Unit]
Description=Run my task daily

[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true

[Install]
WantedBy=timers.target

OnCalendar で cron 的なスケジュールを指定します。 Persistent=true は、OnCalendar= タイマーで発火タイミングを逃した場合に、次回起動時にすぐ実行してくれる設定です(OnBootSec= 等のモノトニックタイマーには効果がありません)。

設定ファイルを作ったら有効化します:

systemctl --user daemon-reload
systemctl --user enable my-task.timer
systemctl --user start my-task.timer

有効化の確認:

systemctl --user list-timers

linger の設定を忘れずに

user インスタンスはデフォルトだとログアウト時に止まります。 ログアウト後も timer を動かしたい場合(サーバーやホームラボなど)は loginctl enable-linger が必要です。

loginctl enable-linger $USER

これで、ログインセッションが存在しなくてもユーザーインスタンスが動き続けます。 セットアップ後に「なぜか動かない」ってなったら、大抵これが抜けています。

Git で設定ファイルを管理する

systemd の設定ファイルもコードなので、Git で管理したくなります。 実体をリポジトリに置いて、シンボリックリンクで ~/.config/systemd/user/ に繋ぐのが便利です。

# リポジトリ内に実体を置く
mkdir -p ~/repos/github/my-project/systemd

# シンボリックリンクを張る
ln -s ~/repos/github/my-project/systemd/my-task.service ~/.config/systemd/user/my-task.service
ln -s ~/repos/github/my-project/systemd/my-task.timer ~/.config/systemd/user/my-task.timer

変更がリポジトリに残るし、別マシンへの移行もやりやすくなります。

4 日間気づかなかったやつ

ここが本題というか、反省点です。

ディレクトリ構成を整理して、scripts/automation/scripts/ に移したんですよ。 そのとき、サービスファイルの ExecStart のパスを更新し忘れました。

タイマー自体は動いているので systemctl --user list-timers では正常に見える。 でも実際にはスクリプトが見つからずにエラーで落ちている。

4 日後に「あれ、出力ファイルが更新されてないな」って気づいて調べたら、ずっとエラーだったことがわかりました。

systemctl --user status my-task.service
journalctl --user -u my-task.service --since "5 days ago"

こういうときのチェックコマンドです。 systemctl --user list-timersLAST 列の時刻が古くなっていたら、何か詰まっているサインです。

プロパティとして取得したい場合は以下のコマンドも使えます。

systemctl --user show my-task.timer -p LastTriggerUSec --value

ネットワーク依存タスクの注意点

user インスタンスでは network.targetnetwork-online.target への依存が期待通りに動かないことがあります。これらは system インスタンス側のユニットで、user manager からは直接参照できないためです。

ネットワークを使うタスクの場合は、service ファイルに依存を書くのではなく、スクリプト側でリトライを入れるのが確実です。

# スクリプト内でリトライ
for i in $(seq 1 5); do
  curl -sf https://example.com && break
  sleep 5
done

まとめ

user timer を使い始めてから、個人の自動化タスクがかなり整理されました。

cron より設定が明示的で、ログも journalctl --user で追いやすい。 root 不要なので、自分のホームラボ環境に気軽に追加できます。

4 日間気づかなかった件から学んだのは、タイマーが動いているように見えてもサービスが実際に成功しているかは別で確認が必要、ということ。 list-timers で LastTriggerUSec を定期的に眺める習慣をつけました。

参考