Claude CodeでsystemdのUnitファイルを作成する手順
Claude CodeにsystemdのUnitファイルを生成させ、Type=やRestart=の意味を確認しながら配置・検証・登録まで進める手順です。
Claude CodeにsystemdのUnitファイルを書かせると、雛形の生成自体はすぐに終わります。ただしExecStart=の絶対パス要件やType=の選び方、起動順序の指定を把握していないと、生成された内容をそのまま検証できません。この記事では、サービス用のUnitファイルを例に、生成から配置・検証・登録までを手順化し、後半でClaude Code自身をsystemdのタイマーから動かす書き方も扱います。
Claude CodeでsystemdのUnitファイルを書くとはどんな作業か
systemdは、Linuxシステムでプロセス1番(PID 1)として動作するシステム・サービスマネージャーです。Unitファイルはiniスタイルのテキストファイルで、サービスやソケット、マウントポイントなど、systemdが管理する対象の設定を記述します。サービスを対象にしたUnitファイルは.serviceという拡張子を持ち、共通の[Unit]・[Install]セクションと、サービス固有の[Service]セクションで構成されます。
Claude Codeに書かせる場合も、精度を左右するのは伝える条件の具体性です。実行したいコマンドの絶対パス・実行ユーザー・再起動時の挙動を先に固めてから生成させます。この進め方は、Claude Codeでk6の負荷テストスクリプトを作成する手順で紹介したテストスクリプトの生成でも同じです。Claude Code自体の機能や料金はClaude Code(クロードコード)とはにまとめています。
前提条件
systemdは多くのLinuxディストリビューションで使われているため、通常は追加インストールが要りません。バージョンは次のコマンドで確認できます。
systemctl --version登録先は、システム全体とユーザー単位の2通りがあります。
システム全体かユーザーモードか
システム全体
Unitファイルは/etc/systemd/system/に置き、systemctlで操作します。この記事の手順は、以降こちらを前提にします。
ユーザーモード
Unitファイルは~/.config/systemd/user/に置き、systemctl --userで操作します。呼び出したユーザー自身のサービスマネージャーに話しかけるため、システム全体には影響しません。ただしProtectSystem=のようにファイルシステムの名前空間を使う設定は、ユーザーサービスでは使えません。
手順1: サービスUnitの雛形をClaude Codeに生成させる
まず、実行したいコマンドの絶対パスと、起動時の依存、落ちたときにどう扱いたいかを具体的に伝えます。
/opt/myapp/bin/workerを実行し続けるsystemdのserviceユニットファイルを書いて。設定は/etc/myapp/config.yamlを読む。起動時に外部APIへ接続するので、ネットワークが使えるようになってから起動したい。プロセスが落ちたら自動で再起動させたいこの指示なら、次のような雛形になります。
[Unit]
Description=My App Worker
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
ExecStart=/opt/myapp/bin/worker --config /etc/myapp/config.yaml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetDescription=はsystemctl statusなどに表示される人間向けの識別名で、ユニット名を単に言い換えるのではなく、そのサービスが何かを示す短い文にします。
起動順序では、After=network.targetを選びたくなる場面ですが、ここに落とし穴があります。外部と通信する前提のサービスは、次の2つの違いを押さえておきます。
network.targetとnetwork-online.target
network.target
ネットワーク機能を提供する側が引き込むパッシブなユニットで、通常は起動をあまり遅らせません。ネットワークの準備完了を保証する指定ではありません。
network-online.target
使う側がWants=で引き込み、After=で順序も並べるアクティブなユニットです。ネットワークが十分に設定されるまで待つ代わりに、起動が大きく遅れることがあります。
After=は起動順序だけを決める設定で、依存そのものは作りません。起動自体を前提にしたいならWants=(緩い依存)かRequires=(強い依存)を併用します。Wants=で指定した対象が起動に失敗しても、このユニットは起動を続けられるのに対し、Requires=は対象の失敗が伝播しうる点が違いです。上の雛形がAfter=とWants=を対にしているのはそのためです。
[Install]セクションのWantedBy=multi-user.targetは、systemctl enableを実行したときにmulti-user.target.wants/へ作られるシンボリックリンク先を指定しており、起動時に自動起動させるかどうかを決めます。
手順2: [Service]セクションの主要ディレクティブを調整する
生成された雛形のType=とRestart=は、サービスの性質に合わせて調整が必要です。公式ドキュメントは、長時間稼働するサービスにはType=execを推奨しています。Type=simple(既定)は、ExecStart=で指定したバイナリのfork()直後にサービスを起動済みとみなします。指定した実行ユーザーが存在しないなどの理由でバイナリが実行できなくても、systemctl startは成功として報告されます。Type=execはバイナリのexecve()成功まで待つため、この種の起動失敗を確実に検知できます。
| Type= | 起動完了のタイミング | 向いている用途 |
|---|---|---|
| simple(既定) | 起動完了のタイミングプロセスのfork直後 | 向いている用途起動確認が不要な常駐プロセス |
| exec | 起動完了のタイミングバイナリのexecve成功後 | 向いている用途長期稼働サービス(公式推奨) |
| forking | 起動完了のタイミング親プロセスの終了後 | 向いている用途従来型のUNIXデーモン |
| oneshot | 起動完了のタイミングメインプロセスの終了後 | 向いている用途一度だけ実行するタスク |
| notify / notify-reload | 起動完了のタイミングREADY=1通知の受信後 | 向いている用途起動完了を自己申告するデーモン |
| dbus | 起動完了のタイミングD-Busバス名の取得後 | 向いている用途D-Busインターフェースを持つサービス |
Restart=は、どの終了状態で再起動するかを決めます。on-failureは非ゼロの終了コードやシグナルによる異常終了、タイムアウト、ウォッチドッグ超過で再起動します。終了コード0などのクリーンな終了では再起動しません。
alwaysはクリーンな終了かどうかを問わず再起動するため、プロセスが自分の判断で正常終了しても再起動が続きます。処理が終わるたびに終了するバッチ的なプロセスに付けると、意図せず再起動を繰り返します。自分の判断で終了する処理が非ゼロで終わっても再起動させたくないときは、シグナル・タイムアウト・ウォッチドッグ超過だけで再起動するon-abnormalが代替になります。なおType=oneshotのサービスには、alwaysもon-successも指定できません。
どの設定でも、systemctl stopのようなsystemd自身の操作で止めた場合は再起動しません。長期稼働サービスには、エラー時の自動復旧を目的としてon-failureを使うことが公式ドキュメントで推奨されています。
再起動の間隔と、再起動を打ち切る条件には既定値があります。
再起動まわりの既定値
RestartSec=
100ms
再起動までの待機時間
StartLimitIntervalSec=
10s
起動回数を数える期間
StartLimitBurst=
5回
期間内に許される起動回数
期間内に回数を超えて起動しようとしたユニットは、それ以上起動を許可されません。待機が100msのままクラッシュを繰り返すと、10秒の間に5回に達して再起動が止まります。雛形のRestartSec=5なら、10秒間に5回の起動には届きません。RestartSec=を縮めるときは、この打ち切りも一緒に考えます。
ExecStart=に指定するコマンドの先頭要素は、絶対パスかスラッシュを含まないファイル名のいずれかである必要があります。相対パスは受け付けられず、ファイル名だけを書いた場合は/usr/local/bin/や/usr/bin/など、コンパイル時に決まった検索パスから解決されます。~もシェルのようには展開されないため、ホーム配下の実行ファイルは/home/<ユーザー名>/から始まる絶対パスで書きます。Claude Codeに生成させた後も、この一点は必ず目で確認します。
実行ユーザーを変えたい場合はUser=(グループはGroup=)を指定します。システムサービスの既定の実行ユーザーはrootのため、権限を絞りたい場合は明示的に指定します。環境変数をまとめて渡したい場合はEnvironmentFile=で外部ファイルを読み込めます。
本番環境で動かす場合は、[Service]セクションに簡単なハードニング設定を加えておくと安全性が上がります。NoNewPrivileges=trueを指定すると、setuidビットなどによる権限昇格をプロセスとその子プロセス全体で禁止できます。ProtectSystem=strictを指定すると、/usr/や/etc/を含むファイルシステムを(/dev/・/proc/・/sys/のAPI用サブツリーを除いて)読み取り専用でマウントし、意図しない書き込みを防ぎます。書き込みが必要なディレクトリだけReadWritePaths=で個別に許可します。PrivateTmp=trueを指定すると、/tmp/と/var/tmp/が他のプロセスと共有されない専用の名前空間に切り替わります。これらはいずれも既定では無効です。
相対パスで設定ファイルや出力先を参照させたい場合は、WorkingDirectory=で実行時のカレントディレクトリを明示しておくと、ExecStart=側の指定がシンプルになります。指定しなければ、システムサービスでは/が既定のカレントディレクトリです。指定先のディレクトリがまだ存在しない可能性があるなら、先頭に-を付けておくと、ディレクトリが無くても起動失敗として扱われなくなります。
手順3: 配置してsystemdに登録・検証する
生成したUnitファイルを/etc/systemd/system/myapp.serviceのように保存したら、まず文法エラーやexec対象の有無をsystemd-analyze verifyで確認します。
sudo systemd-analyze verify /etc/systemd/system/myapp.servicesystemd-analyze verifyは、未知のディレクティブ・起動に必要な依存の欠落・ExecStart=に指定したコマンドが実行できないことなどを警告として表示します。エラーが出なければ、systemdに新しいUnitファイルの存在を認識させます。
sudo systemctl daemon-reloaddaemon-reloadはUnitファイルを再読み込みし、依存関係のツリーを再構築するコマンドです。Unitファイルを新規作成したときや編集したときに実行します。実行を忘れると、systemdは古い定義のままサービスを扱い続け、変更が反映されません。次に、起動時の自動起動を有効化しつつ、今すぐ起動します。
sudo systemctl enable --now myapp.serviceenableは[Install]セクションに基づいてシンボリックリンクを作るだけの操作で、start(起動)とは独立しています。--nowを付けるか別途startを実行しない限り、enableだけではプロセスは動きません。状態はsystemctl statusで確認します。
systemctl status myapp.serviceログを継続的に見たい場合はjournalctlを使います。
journalctl -u myapp.service -f生成したUnitファイルをリポジトリで管理している場合は、変更をコミットしてPRにまとめる流れも任せられます。差分の要約からPR作成までの手順はClaude CodeでPRを作成する手順にまとめています。
Claude Code自身をsystemdのタイマーで定期実行する
常駐させたいプロセスではなく、Claude Codeの非対話実行(claude -p)を夜間などに回したい場合は、Type=oneshotのサービスとタイマーの組み合わせが向きます。v2.1.289のclaude --helpには、無人実行に使える次のフラグが載っています。
--bare: フック・プラグイン・自動メモリー・CLAUDE.mdの自動読み込みを省く。Anthropic APIでは認証をANTHROPIC_API_KEY(またはapiKeyHelper)で行い、サブスクリプションのログインは読まれない(Amazon BedrockやGoogle Cloudなどは各自の認証情報を読む)--allowedTools: 許可するツールを列挙する--permission-mode dontAsk: 許可されていない操作を、確認で止まらず自動的に拒否する--max-budget-usd: 1回の実行で使うAPI費用の上限(--printと併用)--no-session-persistence: セッションをディスクに残さない
systemdから動かす3つの部品
- 1
APIキーを環境ファイルに置く
--bareはAPIキー認証だけを使うため、/etc/myapp/claude.envにANTHROPIC_API_KEY=の1行を書き、EnvironmentFile=で読み込みます。キーを含むので、ファイルの権限は絞っておきます。 - 2
サービスはoneshotにする
実行が終わったら止まる処理なので、
Restart=は付けません。Type=oneshotはタイムアウトが既定で無効のため、応答が返らない実行に備えてTimeoutStartSec=を足します。 - 3
起動時刻はタイマーに持たせる
時刻の指定は
.timer側のOnCalendar=に書き、有効にするのはサービスでなくタイマーです。
サービス側のUnitは次のようになります。claudeの場所はcommand -v claudeで確かめます。ネイティブインストーラーで入れた場合の配置先は~/.local/bin/claudeです。ただしExecStart=では~が使えないため、展開した絶対パスを書きます。
[Unit]
Description=Claude Code nightly README summary
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=deploy
WorkingDirectory=/srv/myapp
EnvironmentFile=/etc/myapp/claude.env
TimeoutStartSec=15min
ExecStart=/home/deploy/.local/bin/claude --bare -p "README.mdを要約して" \
--allowedTools "Read" --permission-mode dontAsk \
--max-budget-usd 1 --no-session-persistence --output-format jsonWorkingDirectory=は、Claude Codeがファイルを読むプロジェクトの場所を決めます。--output-format jsonを付けると、応答にtotal_cost_usdを含むJSONが返るため、journalctlのログから費用の見積もりを追えます。この値はクライアント側の推定で、実際の請求額とは差が出ることがあります。
タイマー側のUnitは、同名の.timerファイルとして置きます。
[Unit]
Description=Run Claude Code nightly summary
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.targetPersistent=trueにすると、タイマーが停止していた間に実行予定が1回以上あった場合、有効化したときにすぐ実行されます。式の書き方に自信がないときは、systemd-analyze calendarで次の実行時刻を確かめます。OnCalendarの書き方や時刻のずらし方はClaude Codeをsystemd timerで定期実行する記事で扱っています。
systemd-analyze calendar "*-*-* 03:00:00"
sudo systemctl daemon-reload
sudo systemctl enable --now claude-summary.timer
systemctl list-timers claude-summary.timerここでは、2つのファイルをclaude-summary.serviceとclaude-summary.timerの名前で置いた前提です。
よくあるつまずき
起動直後に外部APIへ接続できず落ちる。After=network.targetだけを指定していると、ネットワークの準備完了を待たずに起動します。外部と通信するサービスにはAfter=とWants=でnetwork-online.targetを指定します。
クラッシュを繰り返したあと、再起動されなくなる。RestartSec=を短くしたまま既定の起動回数制限(10秒に5回)に当たると、ユニットはそれ以上起動を許可されません。間隔を空けるか、StartLimitIntervalSec=とStartLimitBurst=を見直します。
タイマーを作ったのに実行されない。有効にしたのがサービスだけで、.timerをenableしていない場合です。systemctl list-timersに表示されるかどうかで見分けられます。
oneshotの実行が終わらない。Type=oneshotは既定でタイムアウトがないため、Claude Codeが応答待ちで止まるとユニットが起動中のまま残ります。TimeoutStartSec=で上限を決めます。
まとめ
Claude Codeに書かせたUnitファイルは、ExecStart=の絶対パス・Type=・Restart=・起動順序の4点を目で確かめてから、systemd-analyze verifyとdaemon-reloadを経てenable --nowで登録します。Claude Code自身を定期実行する場合は、--bareと--max-budget-usdで実行を絞り、サービスをoneshot、時刻の指定をタイマーに分けると、止まらない実行や費用の取りこぼしを避けられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- systemd.io/
- freedesktop.org/software/systemd/man/latest/systemd.unit.html
- freedesktop.org/software/systemd/man/latest/systemd.service.html
- freedesktop.org/software/systemd/man/latest/systemctl.html
- freedesktop.org/software/systemd/man/latest/systemd.exec.html
- freedesktop.org/software/systemd/man/latest/systemd-analyze.html
- freedesktop.org/software/systemd/man/latest/systemd.special.html
- freedesktop.org/software/systemd/man/latest/systemd.timer.html
- freedesktop.org/software/systemd/man/latest/systemd-system.conf.html
- code.claude.com/docs/en/headless