systemdへのサービス登録とは、アプリの起動方法を書いた「unitファイル」を /etc/systemd/system/ に置き、systemctl enable --now で有効化して、OS起動時の自動起動や異常終了時の自動再起動をsystemdに任せることです。最小構成なら ExecStart= の1行でも動きますが、本番では「どのユーザーで動かすか」「環境変数をどこから読むか」「落ちたときに何回再起動するか」「どこまでファイルに触れてよいか」まで決めておくと安心です。

この記事では、そのまま使える本番用の雛形と各項目の意味表、Type= の選び方、登録から確認までの流れ、cronの代わりに使えるtimer、ログイン中のユーザーだけで動かす --user サービスまでをまとめました。unitファイルは systemd-analyze verify と systemd-analyze security で実際に検証した出力つきです(systemd 255/Ubuntu 24.04。検証環境はsystemdがPID 1で動かないコンテナのため、systemctl start や journalctl の出力は公式ドキュメントに沿った例として示し、その旨を明記しています)。

systemdサービス登録の早見表

やりたいこと コマンド 補足
unitファイルを置く sudo vi /etc/systemd/system/myapp.service 自作unitはここに置く
書式を検証する systemd-analyze verify /etc/systemd/system/myapp.service 登録前に誤りを見つける
変更を読み込ませる sudo systemctl daemon-reload unitを編集したら毎回
自動起動を有効化+今すぐ起動 sudo systemctl enable --now myapp enableだけでは今は起動しない
状態を見る systemctl status myapp 直近のログも数行出る
ログを見る journalctl -u myapp -f -f で追いかけ表示
再起動・停止 sudo systemctl restart myapp / stop
無効化 sudo systemctl disable --now myapp 停止と自動起動の解除を同時に
一部だけ上書き sudo systemctl edit myapp drop-inファイルが作られる
安全性の採点 systemd-analyze security myapp 0(安全)〜10(無防備)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「systemdサービス登録|本番用unitファイル雛形と設定項目の意味」

登録から確認までの流れ

flowchart LR
    A[unitファイルを作成] --> B[systemd-analyze verify]
    B --> C[systemctl daemon-reload]
    C --> D[systemctl enable --now]
    D --> E[systemctl status]
    E --> F[journalctl -u]
    F -->|直したら| A

ポイントは「編集したら daemon-reload」です。systemdはunitファイルを読み込んだ内容で動くので、ファイルを直しただけでは反映されません。

本番用unitファイルの雛形

Pythonで書いた小さなWebアプリ(/opt/myapp/app.py、ポート番号を環境変数 PORT から読む)を例にします。まず専用ユーザーと設定ファイルを用意します。

sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo mkdir -p /etc/myapp
echo "PORT=8080" | sudo tee /etc/myapp/myapp.env
sudo chmod 640 /etc/myapp/myapp.env
sudo chown root:myapp /etc/myapp/myapp.env
# /etc/systemd/system/myapp.service
[Unit]
Description=My sample web app
Documentation=https://example.com/docs
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=exec
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s

NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
LogsDirectory=myapp

[Install]
WantedBy=multi-user.target

この雛形を置いて検証したところ、何も出力されず終了コード0でした(問題なしの意味)。

$ systemd-analyze verify /etc/systemd/system/myapp.service
$ echo $?
0

各項目の意味

セクション 項目 意味・決め方
Unit Description systemctl status に表示される説明
Unit After= / Wants=network-online.target ネットワークの準備ができてから起動する。Afterは順番だけ、Wantsは起動の依頼なので両方書く
Unit StartLimitIntervalSec / StartLimitBurst 「300秒以内に5回」起動に失敗したら再起動をあきらめる
Service Type=exec 起動の完了をどう判断するか(次の章)
Service User / Group rootではなく専用ユーザーで動かす
Service WorkingDirectory 相対パスの基準。設定ファイルを相対パスで読むアプリで必要
Service EnvironmentFile KEY=VALUE 形式の環境変数ファイル。パスの前に - を付けると、ファイルがなくてもエラーにしない
Service ExecStart 起動コマンド。絶対パスで書くのが確実
Service Restart=on-failure 異常終了(0以外の終了コード、シグナル、タイムアウト)のときだけ再起動
Service RestartSec=5s 再起動までの待ち時間。既定は100ms
Service TimeoutStopSec 停止を待つ時間。過ぎると強制終了
Service NoNewPrivileges=yes setuidなどで権限が増えるのを防ぐ
Service PrivateTmp=yes このサービス専用の /tmp を使う
Service ProtectSystem=strict OS全体を読み取り専用にする
Service ProtectHome=yes /home や /root を見えなくする
Service StateDirectory=myapp /var/lib/myapp を作り、所有者を設定し、書き込みを許可する
Service LogsDirectory=myapp /var/log/myapp を同様に用意する
Install WantedBy=multi-user.target 通常の起動(マルチユーザー)で自動起動する

Restart と StartLimit の関係

RestartSec の既定は100ms、StartLimitの既定は「10秒間に5回」(systemdの既定設定)です。設定ミスで即座に落ちるアプリは、既定のままだと1秒もかからず上限に達して止まります。逆に RestartSec=5s だけを設定すると10秒間に5回には届かないため、いつまでも再起動を繰り返すことになります。

設定 動き
既定のまま 100msごとに再起動 → すぐ上限に達して停止
RestartSec=5s のみ 10秒で最大2〜3回 → 上限に届かず再起動を続ける
RestartSec=5s + StartLimitIntervalSec=300 + StartLimitBurst=5 5分間に5回失敗したら停止して、調査を待つ

「一時的な不調は自動で回復、設定ミスは早めに止めて気づく」という意図で、雛形は3つをセットにしています。なお、StartLimit系は [Service] ではなく [Unit] セクションに書きます。

Type= の選び方

Type systemdが「起動完了」とみなすタイミング 向いているもの
simple(既定) プロセスをforkした直後 手軽に書きたいとき
exec 実行ファイルの起動に成功した直後 常駐するアプリ全般(公式推奨)
notify アプリが sd_notify で READY=1 を送ったとき 準備完了を正確に伝えたいアプリ
notify-reload notifyと同じ+reloadでSIGHUPを送る(v253以降) 設定の再読み込みに対応したアプリ
forking 起動したプロセスが子を残して終了したとき 自分でデーモン化する従来型のソフト
oneshot プロセスが終了したとき 一度だけ実行する処理、timerから呼ぶ処理

systemd.service(5) のマニュアルには、常駐サービスには Type=exec を使うことが推奨と書かれています。simple では実行ファイルのパス間違いがあっても systemctl start は成功扱いになりますが、exec なら起動コマンドの時点で失敗として返るためです。

forking を使う場合は、PIDFile= でメインプロセスを知らせると確実です。多くのアプリには「フォアグラウンドで動くオプション」があるので、そちらを使って exec にするほうが管理しやすくなります。

systemd-analyze verify で誤りを見つける

登録前に書式を確認しておくと、「起動しない原因がunitの書き間違いだった」を減らせます。わざと誤りを入れたunitで検証した実際の出力です。

# bad.service(抜粋)
[Service]
Type=simple
ExecStart=python3 app.py
Restart=always
RestartSecs=5
$ systemd-analyze verify /etc/systemd/system/bad.service
/etc/systemd/system/bad.service:9: Unknown key name 'RestartSecs' in section 'Service', ignoring.

項目名の打ち間違いは「無視」されるだけで、起動自体はしてしまいます。気づかないまま「再起動の間隔が効いていない」状態になるので、verifyで拾っておくと安心です。存在しない実行ファイルを指定した場合は、次のように終了コード1になりました。

$ systemd-analyze verify /etc/systemd/system/bad2.service
bad2.service: Command /opt/myapp/not-exist.sh is not executable: No such file or directory
$ echo $?
1

セキュリティ設定を点数で確認する

systemd-analyze security は、unitの設定から「どれだけ守られていないか」を0〜10で採点します(小さいほど安全)。3つのunitを --offline=yes で採点しました(実際の出力の最終行)。

# ExecStartだけの最小構成
→ Overall exposure level for plain.service: 9.4 UNSAFE :-{

# 上の雛形(User, NoNewPrivileges, ProtectSystem など)
→ Overall exposure level for myapp.service: 8.3 EXPOSED :-(

# 雛形+下の追加設定
→ Overall exposure level for myapp-hard.service: 1.7 OK :-)

雛形の時点で、主要な項目は次のように合格になっています。

✓ User=/DynamicUser=     Service runs under a static non-root user identity
✓ NoNewPrivileges=       Service processes cannot acquire new privileges
✓ ProtectSystem=         Service has strict read-only access to the OS file hierarchy
✓ PrivateTmp=            Service has no access to other software's temporary files
✓ ProtectHome=           Service has no access to home directories

1.7まで下げたときに追加した設定です。

PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
CapabilityBoundingSet=
UMask=0027

点数はあくまで目安で、制限を強めるほどアプリが動かなくなる可能性も上がります。たとえば MemoryDenyWriteExecute=yes はJIT(実行時コンパイル)を使うJavaやNode.jsの動作を妨げることがあります。1つずつ追加して、動作確認しながら進めるのがおすすめです。

登録して動かす

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
journalctl -u myapp -n 50 --no-pager

systemctl status では、Active: active (running) になっていれば起動しています。次は公式ドキュメントに沿った表示の例です(この検証環境ではsystemdがPID 1でないため未実行)。

● myapp.service - My sample web app
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
     Active: active (running) since ...
   Main PID: 1234 (python3)
表示 意味 次の確認
active (running) 動作中 完了
activating (auto-restart) 落ちて再起動待ち journalctl -u myapp で終了理由を見る
failed (Result: exit-code) 0以外で終了 アプリのエラーメッセージを見る
failed (Result: start-limit-hit) 再起動の上限に達した 原因を直して systemctl reset-failed myapp
Loaded: ... ; disabled 自動起動が無効 systemctl enable myapp

journalctl の便利なオプション

目的 コマンド
追いかけ表示 journalctl -u myapp -f
今回の起動以降 journalctl -u myapp -b
時間で絞る journalctl -u myapp --since "1 hour ago"
エラー以上だけ journalctl -u myapp -p err

アプリが標準出力に書いたログは、特に設定しなくてもjournalに記録されます。Pythonはバッファリングで出力が遅れることがあるので、print(..., flush=True) か環境変数 PYTHONUNBUFFERED=1 を使うと即座に反映されます。

設定を変えるときは drop-in で上書き

パッケージで入ったunit(/usr/lib/systemd/system/ 側)を直接編集すると、更新で上書きされます。systemctl edit を使うと、差分だけを別ファイルに書けます。

sudo systemctl edit myapp
# /etc/systemd/system/myapp.service.d/override.conf
[Service]
Environment=LOG_LEVEL=debug
RestartSec=10s

systemctl edit は保存時に自動で daemon-reload します。ExecStart= を上書きしたいときは、先に空の ExecStart= を書いて既存の値を消してから新しい値を書きます。systemctl cat myapp で、元のファイルとdrop-inを合わせた内容を確認できます。

timerでcronの代わりに定期実行する

timerは「いつ実行するか」を書くunitで、同じ名前の .service を起動します。実行ログがjournalに残り、前回の結果も systemctl status で見られるのが利点です。

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
User=myapp
ExecStart=/opt/myapp/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup every day at 03:30

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=10min

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer   # serviceではなくtimerをenable
systemctl list-timers backup.timer

2つとも systemd-analyze verify で終了コード0を確認しました。時刻の書き方は systemd-analyze calendar で試せます。以下はタイムゾーンをAsia/Tokyoにして実行した実際の出力です。

$ systemd-analyze calendar "Mon..Fri *-*-* 09:00:00" --iterations=3
Normalized form: Mon..Fri *-*-* 09:00:00
    Next elapse: Tue 2026-09-29 09:00:00 JST
       (in UTC): Tue 2026-09-29 00:00:00 UTC
       From now: 8h left
   Iteration #2: Wed 2026-09-30 09:00:00 JST
   Iteration #3: Thu 2026-10-01 09:00:00 JST
cron OnCalendar 意味
30 3 * * * *-*-* 03:30:00 毎日3:30
0 9 * * 1-5 Mon..Fri *-*-* 09:00:00 平日9:00
0 * * * * hourly 毎時0分
*/15 * * * * *:0/15 15分ごと
0 0 1 * * monthly 毎月1日0:00
timerの項目 意味
Persistent=true 電源オフ中に過ぎた実行を、起動後に1回取り戻す
RandomizedDelaySec 実行時刻をランダムにずらし、複数台で集中しないようにする
OnBootSec / OnUnitActiveSec 起動からの経過時間や、前回実行からの間隔で指定する

--user サービス:ログインユーザーだけで動かす

root権限なしで、自分のアカウントだけで常駐させたいときは --user サービスを使います。開発用サーバーや個人のツールに向いています。

# ~/.config/systemd/user/devapp.service
[Unit]
Description=Dev app (user service)

[Service]
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Environment=PORT=8081
Restart=on-failure

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now devapp
journalctl --user -u devapp -f
sudo loginctl enable-linger $USER   # ログアウト後も動かし続ける場合
項目 システムサービス --user サービス
置き場所 /etc/systemd/system/ ~/.config/systemd/user/
操作 sudo systemctl ... systemctl --user ...
自動起動の対象 multi-user.target default.target
動く期間 OS起動中ずっと ログイン中(lingerを有効にすればログアウト後も)
User= 使える 使わない(自分自身で動く)

--user サービスで WantedBy=multi-user.target と書くと自動起動しません。default.target を指定します。

よくあるつまずきと対処

症状 原因 対処
編集したのに反映されない daemon-reload していない sudo systemctl daemon-reload
手動では動くのにサービスだと動かない 環境変数・カレントディレクトリ・PATHが違う EnvironmentFile、WorkingDirectory、絶対パスを設定
status=203/EXEC 実行ファイルが見つからない・実行権限がない パスと chmod +x、シバン行を確認
status=217/USER User= のユーザーが存在しない useradd で作成
start-limit-hit 短時間に失敗を繰り返した 原因を直して systemctl reset-failed
再起動後に起動していない enable していない systemctl enable --now
Permission denied でファイルに書けない ProtectSystem=strict が効いている StateDirectory= や ReadWritePaths= で許可
ログが出ない・遅れる 標準出力のバッファリング PYTHONUNBUFFERED=1 など

よくある質問

Q. /etc/systemd/system/ と /usr/lib/systemd/system/ の違いは? A. /usr/lib/systemd/system/ はパッケージが置く場所、/etc/systemd/system/ は管理者が置く場所です。同じ名前があれば /etc 側が優先されます。自作のunitやカスタマイズは /etc 側に置きます。

Q. Restart=always と on-failure はどちらがよいですか? A. 常駐アプリならon-failure が扱いやすいです。always は正常終了(終了コード0)でも再起動するため、意図して止めたプロセスも起動し直します。systemd.service(5) でも常駐サービスには on-failure が推奨されています。

Q. 環境変数は Environment= と EnvironmentFile= のどちらに書くべきですか? A. パスワードやAPIキーは EnvironmentFile= に分け、ファイルの権限を絞るのがおすすめです。unitファイル本体は systemctl cat などで誰でも読めることがあるためです。

Q. Dockerコンテナの中でsystemdは使えますか? A. 通常のコンテナではsystemdはPID 1で動いていないため、systemctl は「System has not been booted with systemd as init system」と表示して動きません(この記事の検証環境でも同じ表示でした)。コンテナではアプリを直接PID 1として起動するのが一般的です。

まとめ

  • サービス登録は「unitファイルを置く → daemon-reload → enable --now → status / journalctl」の流れ
  • 本番では User・EnvironmentFile・WorkingDirectory・絶対パスの ExecStart を必ず書く
  • Restart=on-failure + RestartSec + StartLimit*([Unit]側)をセットにして、回復と停止の線を引く
  • 常駐アプリは Type=exec、一度きりの処理は Type=oneshot
  • 登録前に systemd-analyze verify、守りの強さは systemd-analyze security で確認する
  • 定期実行は timer(OnCalendar + Persistent=true)、個人用は --user + default.target

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「systemdサービス登録|本番用unitファイル雛形と設定項目の意味」

株式会社RJCでは、サービスを安定して動かすための設定を、プロジェクトの中で仲間と学び合いながら実践しています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?