systemd サービスが起動しない - Failed to start の診断手順

systemd サービスが起動しない - Failed to start の診断手順

この記事で解決できること

  • systemctl startFailed to start で失敗する原因を切り分けられる
  • statusjournalctlどこを読むか が分かる
  • exit code・ExecStart のパス・権限・依存関係・start-limit の 定番ハマりどころ を順に潰せる

結論(切り分けの型)

Failed to start の原因はほぼ次の流れで特定できる。上から順に確認する。

  1. systemctl status で状態と Result 行を読む
  2. journalctl -xeu で失敗の生ログを精読する
  3. exit code を読む203/EXEC 200/CHDIR 217/USER 等は systemd 固有の意味を持つ)
  4. ExecStart のパス・実行権限・ユーザー・作業ディレクトリを確認する
  5. 依存・start-limit・unit 編集後の daemon-reload 漏れを潰す

前提(対象環境)

  • systemd 採用ディストリ(Ubuntu / Debian / RHEL / CentOS / Fedora 等)
  • サービス名は例として myapp.service を使用。自分のサービス名に読み替える
  • システムサービス(root 管理)を主対象とする。ユーザーサービスは --user を付ける

まず何を見ればいいのか?

結論: 起点は systemctl status <service>Active: 行の状態、Main PID の exit code、末尾の直近ログ 10 行で当たりがつく。failedactivating (auto-restart) かで方向が分かれる。

$ systemctl status myapp
× myapp.service - My Application
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
     Active: failed (Result: exit-code) since Fri 2026-06-05 10:00:01 JST; 5s ago
   Main PID: 12345 (code=exited, status=203/EXEC)
        CPU: 4ms

Jun 05 10:00:01 host systemd[1]: myapp.service: Main process exited, code=exited, status=203/EXEC
Jun 05 10:00:01 host systemd[1]: myapp.service: Failed with result 'exit-code'.

読むべき箇所:

  • Loaded: — unit ファイルのパスと enabled / disabled。ここが not-found なら unit 自体が見つかっていない。
  • Active:failed なら起動して落ちた。activating (auto-restart) なら再起動ループ中。
  • Result:exit-code(プロセスが非0終了)/ timeout(起動が間に合わない)/ signal(シグナルで死亡)/ start-limit-hit(再起動しすぎ)。
  • status=NNN/NAME — exit code。後述のとおり 200 番台は systemd 固有の意味を持つ。

status の表示は端末幅で末尾ログが切れる。全文は journalctl で読む。Result: の値が一次的な分岐点になる。

journalctl で失敗ログをどう読むか?

結論: journalctl -xeu <service> が最重要。-u でサービス限定、-e で末尾へジャンプ、-x で systemd の補足説明が付く。アプリ自身が吐いたエラー(command not found / Permission denied / bind: address already in use 等)がここに残る。

# サービス限定で末尾を読む(最頻出)
$ journalctl -xeu myapp

# 直近の起動分だけに絞る
$ journalctl -u myapp --since "5 min ago"

# 今回のブート以降に限定
$ journalctl -b -u myapp

status=203/EXEC のような systemd 由来コードに対し、アプリ自身のエラーメッセージは journal にしか出ないことが多い。両方を突き合わせる。

ログが空・古い場合の注意点:

  • daemon-reload 漏れ: unit を編集したのに反映されていない(後述)。
  • 時刻ズレ: --since が効かない場合はサーバ時刻を疑う。
  • ユーザーサービス: journalctl --user -u myapp を使う。root の journal には出ない。

exit code 203 / 200 / 217 は何を意味するのか?

結論: systemd は起動準備中の失敗に 200〜243 の固有 exit code を割り当てる。代表は 203/EXEC(実行ファイルが無い・実行権限が無い)、200/CHDIR(WorkingDirectory が無い)、217/USER(User= のユーザーが存在しない)。アプリが返す一般的な終了コードと区別できる。

Main PID 行の status=NNN/NAME を読む。200 番台は「アプリが動く前に systemd 側で失敗した」サインで、原因が unit 設定にあることをほぼ確定できる。

status 名称 典型原因
203/EXEC EXEC ExecStart のパス誤り / 実行権限なし / shebang 不正
200/CHDIR CHDIR WorkingDirectory= のディレクトリが存在しない
217/USER USER User= で指定したユーザーが存在しない
1 (アプリ) アプリ自身が返した一般エラー。journal の本文を読む
# 203/EXEC の切り分け: パスと実行権限を確認
$ systemctl cat myapp | grep ExecStart
ExecStart=/opt/myapp/bin/server --config /etc/myapp.conf

$ ls -l /opt/myapp/bin/server      # 存在するか / x ビットがあるか
$ head -1 /opt/myapp/bin/server    # スクリプトなら shebang を確認

ExecStart先頭は絶対パス必須server のような相対指定・PATH 依存は不可。command not found 相当が 203/EXEC として現れる。

unit ファイルの内容と構文をどう確認するか?

結論: 編集前の元ファイルではなく systemctl cat <service> で「実際に有効な内容」を見る。drop-in(*.d/*.conf)の上書きも合算表示される。構文の妥当性は systemd-analyze verify で機械的に検査できる。

# 実際に効いている unit(drop-in 込み)を表示
$ systemctl cat myapp

# unit ファイルの構文・参照を検証
$ systemd-analyze verify /etc/systemd/system/myapp.service

systemd-analyze verify は存在しない設定ディレクティブ、解決できない依存、ExecStart の不在などを警告する。出力が無ければ構文上の問題はない。

よくある設定ミス:

  • Type= の不一致: フォアグラウンド常駐プロセスなのに Type=forking を指定すると、systemd が子プロセスを待ち続けてタイムアウトする。fork してデーモン化しないなら Type=simple(既定)にする。
  • ExecStart が相対パス: 前節のとおり絶対パスが必須。
  • 環境変数未設定: 対話シェルの .bashrc は読まれない。Environment=EnvironmentFile= で明示する。

Type=forking を使うなら PIDFile= を併記するのが安全。指定が無いと systemd が主プロセスを取り違え、active 表示なのに実体が落ちている状態になりやすい。確信が無ければ Type=simple から始める。

unit を編集したのに反映されないのはなぜか?

結論: systemd は unit ファイルをメモリにキャッシュする。編集後に systemctl daemon-reload を実行しないと旧定義のまま起動する。「直したはずなのに同じエラー」の典型原因。

$ sudo vim /etc/systemd/system/myapp.service
$ sudo systemctl daemon-reload      # ← これを忘れると編集が反映されない
$ sudo systemctl restart myapp

daemon-reload 漏れの状態では systemctl cat が編集後の内容を表示する一方、起動時の挙動は旧定義のまま、という食い違いが起きる。編集→daemon-reloadrestart を 1 セットで習慣化する。

unit ファイルを直接 vim する代わりに systemctl edit myapp(drop-in 作成)/ systemctl edit --full myapp(全体編集)を使うと、保存時に daemon-reload 相当が自動で走る。編集漏れ事故を構造的に防げる。

権限・依存・タイムアウトの切り分け

結論: アプリは動くのにサービスだと落ちる場合、実行ユーザーの権限不足・依存サービスの未起動・起動タイムアウトのいずれかが多い。User= 権限、After=/Requires=TimeoutStartSec を順に確認する。

権限(手動では動くのにサービスで Permission denied)

systemctl startUser=(既定 root)の権限で実行される。手動実行時とユーザーが違えば、ファイル・ポート・ソケットへのアクセスで Permission denied になる。

# サービスの実行ユーザーで手動再現する
$ sudo -u myappuser /opt/myapp/bin/server --config /etc/myapp.conf

これでエラーが再現すれば原因はアプリ/権限側。再現しなければ unit 設定側を疑う。権限の基礎はPermission denied の直し方を参照。

依存関係(先に必要なサービスが上がっていない)

DB や network-online を必要とするのに起動順が保証されていないと、起動直後に接続失敗で落ちる。

[Unit]
After=network-online.target postgresql.service
Wants=network-online.target

After= は順序のみ、Requires=/Wants= は依存関係を表す。「接続先がまだ無い」系の失敗はここを見直す。

タイムアウト(Result: timeout

statustimeout と出る場合、既定 90 秒以内に「起動完了」と systemd に通知できていない。重い初期化を持つサービスは TimeoutStartSec= を延ばすか、Type=notify で準備完了を明示通知する。

再起動ループ「start request repeated too quickly」の対処は?

結論: 短時間に規定回数(既定 StartLimitIntervalSec=10sStartLimitBurst=5 回)失敗すると、systemd は以降の起動を抑止し start-limit-hit を表示する。根本原因を直したうえで systemctl reset-failed でカウンタを解除する。

myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.

このメッセージは結果であって原因ではない。本当の原因は直前の失敗ログにある。手順:

# 1) 本当の失敗理由を遡って読む
$ journalctl -xeu myapp

# 2) 原因(ExecStart / 権限 / 依存 など)を修正

# 3) 失敗カウンタを解除してから起動
$ sudo systemctl reset-failed myapp
$ sudo systemctl start myapp

reset-failedカウンタを消すだけで原因は直さない。先に失敗理由を潰さないと、再び同じループに入って start-limit-hit が再発する。順番を守る。

診断チェックリスト

結論: 「status → journalctl → exit code → unit 内容 → daemon-reload → 権限・依存 → start-limit」の順に上から潰せば、Failed to start の原因はほぼ特定できる。

上から順に確認する。

  • [ ] systemctl status myappActive: / Result: / status=NNN を読んだ
  • [ ] journalctl -xeu myapp でアプリ自身のエラーを確認した
  • [ ] exit code を判定した(203/EXEC 200/CHDIR 217/USER は unit 設定起因)
  • [ ] systemctl cat myapp で有効な unit を確認、ExecStart は絶対パス
  • [ ] systemd-analyze verify で構文を検査した
  • [ ] unit 編集後に systemctl daemon-reload を実行した
  • [ ] Type= がプロセスの挙動(fork するか)と一致している
  • [ ] sudo -u <User> で手動再現し、権限・依存・タイムアウトを切り分けた
  • [ ] start-limit-hit は原因修正後に reset-failed で解除した

次に読む