- 技術 💻
systemdサービス登録|本番用unitファイル雛形と設定項目の意味
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期連続の黒字決算。