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-timers の LAST 列の時刻が古くなっていたら、何か詰まっているサインです。
プロパティとして取得したい場合は以下のコマンドも使えます。
systemctl --user show my-task.timer -p LastTriggerUSec --value
ネットワーク依存タスクの注意点
user インスタンスでは network.target や network-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 を定期的に眺める習慣をつけました。