systemd サービスが起動しない - Failed to start の診断手順
この記事で解決できること
systemctl startが Failed to start で失敗する原因を切り分けられるstatusとjournalctlの どこを読むか が分かる- exit code・
ExecStartのパス・権限・依存関係・start-limit の 定番ハマりどころ を順に潰せる
結論(切り分けの型)
Failed to start の原因はほぼ次の流れで特定できる。上から順に確認する。
systemctl statusで状態と Result 行を読むjournalctl -xeuで失敗の生ログを精読する- exit code を読む(
203/EXEC200/CHDIR217/USER等は systemd 固有の意味を持つ) ExecStartのパス・実行権限・ユーザー・作業ディレクトリを確認する- 依存・start-limit・unit 編集後の
daemon-reload漏れを潰す
前提(対象環境)
- systemd 採用ディストリ(Ubuntu / Debian / RHEL / CentOS / Fedora 等)
- サービス名は例として
myapp.serviceを使用。自分のサービス名に読み替える - システムサービス(root 管理)を主対象とする。ユーザーサービスは
--userを付ける
まず何を見ればいいのか?
結論: 起点は
systemctl status <service>。Active:行の状態、Main PIDの exit code、末尾の直近ログ 10 行で当たりがつく。failedかactivating (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-reload→restart を 1 セットで習慣化する。
unit ファイルを直接 vim する代わりに systemctl edit myapp(drop-in 作成)/ systemctl edit --full myapp(全体編集)を使うと、保存時に daemon-reload 相当が自動で走る。編集漏れ事故を構造的に防げる。
権限・依存・タイムアウトの切り分け
結論: アプリは動くのにサービスだと落ちる場合、実行ユーザーの権限不足・依存サービスの未起動・起動タイムアウトのいずれかが多い。
User=権限、After=/Requires=、TimeoutStartSecを順に確認する。
権限(手動では動くのにサービスで Permission denied)
systemctl start は User=(既定 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)
status で timeout と出る場合、既定 90 秒以内に「起動完了」と systemd に通知できていない。重い初期化を持つサービスは TimeoutStartSec= を延ばすか、Type=notify で準備完了を明示通知する。
再起動ループ「start request repeated too quickly」の対処は?
結論: 短時間に規定回数(既定
StartLimitIntervalSec=10s内StartLimitBurst=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 myappでActive:/Result:/status=NNNを読んだ - [ ]
journalctl -xeu myappでアプリ自身のエラーを確認した - [ ] exit code を判定した(
203/EXEC200/CHDIR217/USERは unit 設定起因) - [ ]
systemctl cat myappで有効な unit を確認、ExecStartは絶対パス - [ ]
systemd-analyze verifyで構文を検査した - [ ] unit 編集後に
systemctl daemon-reloadを実行した - [ ]
Type=がプロセスの挙動(fork するか)と一致している - [ ]
sudo -u <User>で手動再現し、権限・依存・タイムアウトを切り分けた - [ ]
start-limit-hitは原因修正後にreset-failedで解除した