2026年5月21日

2026年5月21日

systemdのユニットファイルを作成する方法(serviceとtimer・自動再起動・上書き設定)

自作のアプリやスクリプトを、サーバーの起動時に自動で立ち上げたい、落ちたら自動で再起動させたい、決まった時刻に実行したい、というときに使うのがsystemdの「ユニットファイル」です。ユニットファイルを置くと、Nginxなどのサービスと同じように systemctl で起動・停止でき、ログは journalctl で読めます。

この記事のコマンドと設定は、2026年10月時点のUbuntu 24.04、Debian 12、AlmaLinux/Rocky Linux 9など、systemdを使う現行のディストリビューションで共通に使えるものです。

ユニットファイルの置き場所

場所 用途
/etc/systemd/system/ 管理者が作るユニット、上書き設定(ここに置く)
/usr/lib/systemd/system/(Debian系では /lib/systemd/system/ も) パッケージが入れるユニット。更新で上書きされるので直接編集しない
~/.config/systemd/user/ ログインユーザーごとのユニット(systemctl --user で操作)

ファイル名は 名前.service のように、種類を表す拡張子で終えます。この記事では myapp.service を例にします。

手順1: 実行用のユーザーを作る

アプリをrootで動かす必要がなければ、専用のユーザーを作ります。ログインできないシステムユーザーにしておきます。

sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo chown -R myapp:myapp /opt/myapp

手順2: serviceユニットを書く

Node.jsのアプリ /opt/myapp/server.js を常駐させる例です。

sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My Application
# ネットワークの設定が済んでから起動する
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment=NODE_ENV=production
EnvironmentFile=-/etc/myapp.env
ExecStart=/usr/bin/node /opt/myapp/server.js
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

ユニットファイルのコメントは、# か ; で始まる行に書きます。Type=simple # 説明 のように値の後ろに書くと、コメントではなく値の一部として扱われるので注意します。

各セクションの役割は次のとおりです。

セクション 主な項目 意味
[Unit] Description systemctl status などに表示される説明
After / Wants 起動の順序と、一緒に起動したいユニット
[Service] Type 起動完了をどう判断するか(次の表)
User / Group 実行するユーザーとグループ
WorkingDirectory 作業ディレクトリ
Environment / EnvironmentFile 環境変数。EnvironmentFile の前の - は、ファイルが無くてもエラーにしない指定
ExecStart 起動するコマンド
Restart / RestartSec 再起動の条件と、再起動までの待ち時間
[Install] WantedBy systemctl enable したとき、どのターゲットから起動されるか。通常のサービスは multi-user.target

ExecStart のプログラムは /usr/bin/node のように / から始まるフルパスで書くことを勧めます。ファイル名だけを書いた場合、systemdはコンパイル時に決まった検索パス(/usr/local/bin や /usr/bin など)から探し、シェルの PATH やログイン時の設定ファイル(.bashrc など)は読みません。nvmなどでユーザーのホームに入れたNode.jsを使う場合は、そのフルパスを書きます。

Typeの選び方

Type 起動完了とみなす時点 向いているプログラム
simple プロセスを起動した直後(ExecStart があり Type を省略したときの既定値) フォアグラウンドで動き続けるもの
exec プログラムの実行に成功した時点 simple と同じ。実行ファイルが無いときに systemctl start が失敗として返る
forking 起動したプロセスが子プロセスを残して終了した時点 自分でデーモン化する古いタイプのプログラム(PIDFile も指定する)
oneshot プロセスが終了した時点 一度実行して終わる処理(バックアップ、timerから呼ぶ処理など)
notify プログラムが準備完了を通知した時点 sd_notify に対応したプログラム

自作のアプリはフォアグラウンドで動かし、Type=exec(または simple)にするのが扱いやすい形です。標準出力と標準エラーに書いたログは、そのままjournalに記録されます。

Restartの選び方

値 再起動する場合
no 再起動しない(既定値)
on-failure 0以外の終了コード、シグナルによる終了、タイムアウトなど異常な終了のとき
on-abnormal シグナルによる終了やタイムアウトのとき(終了コードによる終了では再起動しない)
always 正常終了を含め、systemctl stop 以外で止まったとき

短い間に再起動が続くと、systemdは起動の回数制限(既定では10秒間に5回)にかかったとして再起動をやめ、ユニットを failed にします。制限は [Unit] セクションの StartLimitIntervalSec と StartLimitBurst で変えられます。起動直後に落ち続ける場合は、制限を緩めるより先に journalctl で原因を確かめます。

手順3: 読み込み、有効化して動作を確かめる

# 書式の誤りを確かめる
sudo systemd-analyze verify /etc/systemd/system/myapp.service

# systemd にユニットファイルを読み込ませる
sudo systemctl daemon-reload

# 起動し、サーバー起動時にも自動で起動するようにする
sudo systemctl enable --now myapp.service

# 状態とログを確かめる
systemctl status myapp.service
journalctl -u myapp.service -n 50
journalctl -u myapp.service -f

ユニットファイルを書き換えたら、daemon-reload を実行してから systemctl restart myapp で反映します。daemon-reload を忘れると、systemctl status に「changed on disk. Run 'systemctl daemon-reload' to reload units.」という警告が表示され、古い設定のまま動きます。ログの読み方はjournalctlでsystemdのログを確認する方法を参照してください。

アプリを再起動せずに設定を読み直させたい場合は、アプリが対応していれば ExecReload=/bin/kill -HUP $MAINPID を [Service] に書き、systemctl reload myapp で使います。

既存のユニットを上書きする(drop-in)

パッケージが入れたユニット(Nginx、PHP-FPMなど)を変えたいときは、元のファイルを編集せず、上書き用のファイル(drop-in)を作ります。

sudo systemctl edit nginx.service

エディタが開くので、変えたい項目だけを書いて保存します。内容は /etc/systemd/system/nginx.service.d/override.conf に保存され、保存時に自動で読み込まれます(反映には systemctl restart nginx が必要です)。

[Service]
Restart=on-failure
RestartSec=5s
LimitNOFILE=65535

ExecStart のように複数回書ける項目は、追記扱いになります。置き換えたい場合は、空の ExecStart= で一度消してから書きます。

[Service]
ExecStart=
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'

上書きの結果は systemctl cat nginx.service で確かめられ、sudo systemctl revert nginx.service で上書きを取り消せます。

timerで定期的に実行する

cronの代わりに、timerユニットで定期実行できます。timerは、同じ名前のserviceユニットを起動します。ここではWordPressのWP-Cronを、アクセスに頼らず5分ごとにWP-CLIで実行する例を示します。WP-CLIが /usr/local/bin/wp に入っていて、WordPressが /var/www/html にあり、Webサーバーが www-data ユーザーで動いている環境の例です。

/etc/systemd/system/wp-cron.service:

[Unit]
Description=Run WordPress cron events

[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/wp cron event run --due-now --path=/var/www/html --quiet

/etc/systemd/system/wp-cron.timer:

[Unit]
Description=Run WordPress cron events every 5 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
# 有効にするのは timer のほう
sudo systemctl enable --now wp-cron.timer

# 次の実行予定と前回の実行時刻
systemctl list-timers wp-cron.timer

# 手動で一度実行して結果を確かめる
sudo systemctl start wp-cron.service
journalctl -u wp-cron.service -n 20

timerで動かすようにしたら、wp-config.php に define( 'DISABLE_WP_CRON', true ); を書いて、アクセスのたびにWP-Cronが動く仕組みを止めます。WP-Cronをサーバー側で動かす考え方はWP-Cronをサーバーのcronで実行する方法も参照してください。

時刻を指定する場合は OnCalendar を使います。

[Timer]
# 毎日 3:00 に実行する
OnCalendar=*-*-* 03:00:00
# 電源が切れていて実行できなかった分を、次の起動時に実行する
Persistent=true

OnCalendar の書式は、systemd-analyze calendar '*-*-* 03:00:00' で、次にいつ実行されるかを表示して確かめられます。

権限を絞る設定

サービスが侵害されたときの影響を小さくするため、[Service] に次のような設定を加えられます。

[Service]
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectHome=yes
項目 内容
NoNewPrivileges=yes setuidなどで権限を上げられないようにする
PrivateTmp=yes サービス専用の /tmp を使う
ProtectSystem=full /usr、/boot、/etc などを読み取り専用にする
ProtectHome=yes /home、/root などを見えなくする

アプリがホームディレクトリや /etc に書き込む場合は動かなくなるので、1つずつ加えて動作を確かめます。systemd-analyze security myapp.service で、ユニットの制限の状況を一覧できます。

うまく動かないとき

  • status=203/EXEC: ExecStart のプログラムが見つからないか、実行権限が無い。パスと chmod +x を確かめる
  • status=200/CHDIR: WorkingDirectory が無いか、User で指定したユーザーが入れない
  • status=217/USER: User で指定したユーザーが存在しない
  • 手動では動くのにサービスでは動かない: 環境変数(PATH や HOME など)の違いが多い原因です。必要な値を Environment か EnvironmentFile に書く

まとめ

自作のプログラムをサービスにするには、/etc/systemd/system/ に [Unit]・[Service]・[Install] の3つのセクションを持つ .service ファイルを置き、daemon-reload の後に systemctl enable --now で有効にします。常駐するアプリは Type=exec と Restart=on-failure が扱いやすく、ログは journalctl -u で確かめます。既存のユニットは systemctl edit で上書きし、定期実行はserviceとtimerを組み合わせて作ります。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲