バックアップ,定期バッチ,証明書更新,データ集計── VPS運用には定期実行が欠かせない.ところがこの定期実行ほど「手元では動くのに,cronに載せると動かない」という不可解なトラブルが多い領域もない.

原因はほぼ決まっている.PATHの違い・環境変数の不足・%の特殊扱い・多重起動・ログの不在・タイムゾーンのずれ── この6つだ.これらを知らずに「なぜか動かない」と悩み続ける人が後を絶たない.

この記事は,VPSで定期処理を仕込む個人開発者,cronのトラブルに何度もハマったエンジニアに向けている.cronとsystemd timerの正しい使い分けから,6大ハマりどころの原因と対処までを徹底的に解説する.

読み終えたとき,あなたは定期実行を「祈って待つ」のではなく「確実に動くと確信して仕込める」ようになっている.これは地味だが,運用の信頼性を根本から変えるスキルだ.

なぜ定期実行は「動かない」が多発するのか

最大の理由は,cronが「あなたが普段使っている対話シェルの環境」を一切引き継がないことだ.ログインシェルでは読み込まれる.bashrcやPATH,環境変数が,cronの実行環境には存在しない.

そのため,手元ではnodepythonがPATHで見つかっても,cronの中では「コマンドが見つからない」で静かに失敗する.しかもcronは既定ではエラーを目立つ形で通知しないため,失敗に気づけない.

さらにcronは実行ログを残さないのが既定だ.「動いたのか,失敗したのか,そもそも実行されたのか」が分からず,デバッグが極端に難しくなる.

これらの罠を体系的に潰せば,定期実行は驚くほど安定する.そしてsystemd timerを選べば,これらの罠の多く(ログ・状態確認・停止中の補完)が最初から解消される.まずは両者の使い分けから整理しよう.

cron か systemd timer か ― 使い分けの判断軸

定期実行の手段は主に2つ.枯れていて手軽なcronと,ログ・状態管理・依存制御に優れるsystemd timerだ.どちらが正解ということはなく,処理の重要度で選ぶ.

観点cronsystemd timer
設定の手軽さ◎(1行)△(2ファイル)
実行ログ×(自前で工夫)◎(journaldに自動)
状態/次回時刻の確認×◎(list-timers)
停止中に逃した分の補完×◎(Persistent=true)
多重起動の防止自前◎(サービス単位で制御)
依存関係の制御×◎(After/Requires)

判断軸はシンプルだ.失敗が許されない重要処理(バックアップ・課金バッチ・証明書更新)はsystemd timer,軽量で失敗してもすぐ気づける処理はcronで十分.本記事ではまずcronを正しく使う技術を固め,その上でtimerの優位を活かす流れで解説する.

crontab書式を正確に読む ― 5つのフィールド

crontabの1行は「分 時 日 月 曜日 コマンド」の6要素だ.この順序と各フィールドの範囲を正確に覚えることが第一歩になる.

crontab の書式と例

# ┌── 分 (0-59)
# │ ┌── 時 (0-23)
# │ │ ┌── 日 (1-31)
# │ │ │ ┌── 月 (1-12)
# │ │ │ │ ┌── 曜日 (0-7, 0と7=日曜)
# │ │ │ │ │
  0 3 * * *   /home/deploy/backup.sh      # 毎日3:00
  */15 * * * * /home/deploy/poll.sh        # 15分おき
  0 9 * * 1   /home/deploy/weekly.sh       # 毎週月曜9:00
  0 0 1 * *   /home/deploy/monthly.sh      # 毎月1日0:00

編集はcrontab -e(自分のユーザーのcron)で行い,crontab -lで一覧を確認する.システム全体の/etc/crontabは「ユーザー名」フィールドが追加で入る点が異なるので混同しないこと.

日と曜日を両方指定したときの落とし穴

意外な罠が,日(3番目)と曜日(5番目)を両方指定すると「OR条件」になることだ.0 0 1 * 1は「毎月1日」かつ「毎週月曜」ではなく,「毎月1日または毎週月曜」に実行される.意図しない頻度で動く原因になりやすい.

ハマりどころ1・2 ― PATHと環境変数

最頻出の失敗が,PATHの違いでコマンドが見つからないことだ.対策は2つ.「コマンドを絶対パスで書く」か「crontab冒頭でPATHを定義する」.確実なのは絶対パスだ.

PATHと環境変数を明示する

# crontabの冒頭でPATHと環境を定義
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash

# コマンドは絶対パスが最も確実
0 3 * * * /usr/bin/node /home/deploy/app/batch.js

# .envを読み込みたい場合はシェル経由で
0 4 * * * cd /home/deploy/app && /bin/bash -lc 'set -a; . ./.env; node task.js'

アプリが.envや特定の環境変数に依存している場合,cronの実行環境にはそれが無い.bash -lcでログインシェルを起動して読み込ませるか,スクリプト内で明示的に環境を読み込むのが定石だ.「対話シェルで動く=cronで動く」では決してない,と肝に銘じる.

ハマりどころ3・4 ― %の特殊扱いと多重起動

意外な罠がcronでは%が改行扱いになることだ.date +%Fのような指定はそのままだと壊れる.%とエスケープが必要になる.

%のエスケープと多重起動の防止

# %は % とエスケープする
0 3 * * * tar czf /backup/db-$(date +%F).tgz /data

# flockで多重起動を防ぐ(前回が終わらないうちに次が走る事故を防止)
*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /home/deploy/sync.sh

もう一つの重要対策が多重起動の防止だ.処理が想定より長引くと,前回が終わらないうちに次が起動し,二重実行でデータが壊れることがある.flockで排他ロックを取れば,「前回がまだ動いていたら今回はスキップ」を実現できる.定期同期やバッチでは必須のテクニックだ.

ハマりどころ5・6 ― ログの不在とタイムゾーン

cronは既定でログを残さないため,必ず出力をファイルにリダイレクトする.標準出力と標準エラーの両方を記録するのが鉄則だ.

ログを残し,タイムゾーンを確認する

# 標準出力と標準エラーをログに記録
0 3 * * * /home/deploy/backup.sh >> /var/log/backup.log 2>&1

# サーバーのタイムゾーンを確認(cronはこの時刻で動く)
timedatectl
# 日本時間で動かしたい場合
sudo timedatectl set-timezone Asia/Tokyo

>> ログ 2>&1で追記ログを残せば,「いつ実行され,何を出力し,どこで失敗したか」が追える.そしてタイムゾーンも要注意だ.VPSの初期設定はUTCのことが多く,「3:00指定なのに日本時間の正午に動く」混乱が起きる.timedatectlで確認し,必要ならAsia/Tokyoに設定する.

systemd timerで罠をまとめて回避する

ここまでのcronの罠の多く── ログの不在・状態確認のしにくさ・停止中の取りこぼし・多重起動── は,systemd timerなら標準機能で解消される.重要な定期処理はtimerへ寄せる価値が高い.

重要バッチをtimerで(ログ・補完つき)

# /etc/systemd/system/report.service
[Service]
Type=oneshot
User=deploy
EnvironmentFile=/home/deploy/app/.env
ExecStart=/usr/bin/node /home/deploy/app/report.js

# /etc/systemd/system/report.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true          # 停止中に逃した分を起動後に実行
[Install]
WantedBy=timers.target

timerなら実行ログはjournalctl -u reportで確認でき,EnvironmentFileで環境変数も素直に渡せ,Persistent=trueで停止中の取りこぼしも補える.サービスは同時に一つしか走らないため多重起動も防げる.cronの6大ハマりどころのほとんどが,最初から存在しないのだ.

定期処理が動かない時の全チェックリスト

「設定したのに動かない」とき,上から順に確認すれば原因にたどり着ける.

  1. そもそも実行されているか:ログにリダイレクトしていないなら,まずログを仕込む.journalctl -u cronやtimerのlist-timersで起動を確認.
  2. コマンドのパス:絶対パスになっているか.which nodeの結果をそのまま書く.
  3. 環境変数.envや必要な変数を読み込んでいるか.bash -lcを検討.
  4. %のエスケープdate +%F等が%になっているか.
  5. 権限:スクリプトに実行権(chmod +x)があるか,対象ファイルにアクセス権があるか.
  6. タイムゾーンtimedatectlで意図した時刻基準か.
  7. 多重起動/競合:前回が終わらず詰まっていないか.flockを検討.

補論:定期処理の検証は「使い捨てVPS」で時短する

定期処理のデバッグは時間がかかる.「明日の3時に動くか」を待っていては検証が進まない.実務ではOnCalendarやcron式を一時的に「1分おき」に変えて動作確認し,確認後に本来の時刻に戻す,という時短が効く.

この試行錯誤を安全に回すには,スナップショットで戻せて,動作の速いVPSが向く.設定を散らかしても巻き戻せるし,journalctlでの確認も軽快だ.たとえば 高速NVMe・50種類以上のOSテンプレートに対応した国内VPS─シン・VPS─ はNVMe SSDでバッチ処理やログ確認がきびきび動き,スナップショットで検証環境をすぐ初期化できるため,定期処理の作り込みに向いている.

バックアップや集計の結果を独自ドメインのダッシュボードで確認したくなったら,取り扱い400種類以上のドメイン取得サービス─ムームードメイン─ で取得したドメインをVPSに割り当てると運用が整理しやすい.定期処理と公開を一台で完結できる.

よくある質問(FAQ)

Q1.結局cronとtimerどちらを使えばいい?

失敗が許されない重要処理(バックアップ・課金・証明書更新)はsystemd timer,軽量な処理はcronが指針だ.timerはログ・状態確認・取りこぼし補完が標準で,重要処理ほど恩恵が大きい.

Q2.手元で動くのにcronで動かない最大の原因は?

PATHと環境変数の違いだ.cronは対話シェルの環境を引き継がない.コマンドを絶対パスで書き,必要な環境変数を明示的に読み込めば大半は解決する.

Q3.cronのエラーに気づけない

出力を>> ログ 2>&1でファイルに残すのが基本だ.通知が欲しければ,失敗時にメールやチャットに送る処理をスクリプトに足す.timerならjournaldに自動で残る.

Q4.処理が長引いて二重に走るのが怖い

flockで排他ロックを取り,前回が動作中なら今回はスキップさせる.systemd timerならサービス単位で同時実行が抑止されるため,この問題が起きにくい.

Q5.実行時刻が思った時間とずれる

タイムゾーンを確認する(timedatectl).VPSはUTC初期設定が多く,日本時間で動かすならAsia/Tokyoに設定する.設定後はcron/timerの時刻指定が日本時間基準になる.

Q6.サーバーが停止していた間の実行はどうなる?

cronは飛ぶ(実行されない),systemd timerはPersistent=trueなら起動後に補完される.バックアップのように「必ず1日1回」が要る処理はtimer+Persistentが安心だ.

まとめ ― 定期実行は「祈り」から「確信」へ

定期実行が動かない原因は,PATH・環境変数・%・多重起動・ログ・タイムゾーンの6つにほぼ集約される.これらを潰し,重要処理はsystemd timerに寄せれば,定期処理は確実に動く土台になる.

今日やるべきことは,既存のcronジョブにログ出力を足し,本当に動いているか確認すること.動いていなければ,チェックリストを上から当てれば原因にたどり着ける.

「動いているはず」を「動いていると確認した」に変えること.それが運用の信頼性を一段引き上げる.スナップショットで戻せるVPSがあれば,今夜から安全に検証できる.