VPSのcronが動かない原因は、だいたい書式の勘違いか環境変数だった
毎日18:00にNodeスクリプトを回すcronが動かない。原因は分と時の順番ミス、曜日の0と7、cron環境の最小PATHでnodeが見つからない、の3連発だった。実際のcrontab例とログの残し方をまとめた。
VPSで毎日18:00に案件チェック用のNodeスクリプトを自動実行したくて、crontabを書いた。5分で終わる作業のはずだった。実際には、ちゃんと動くまでに3回ハマった。
しかも質が悪いことに、cronは失敗しても画面には何も出ない。エラーも出ない、ログもない、ただ静かに動かない。だから「書式が合っているのか」「そもそも実行されたのか」の切り分けから始めることになる。今回ハマった3つを、実際のcrontabと一緒に残しておく。
ハマり1: 分と時を逆に書いていた
最初に書いたのがこれ。
18 0 * * * /home/user/scripts/check-jobs.sh
「18時0分」のつもりで書いたが、cronの書式は左から「分 時 日 月 曜日」。つまりこれは「毎日0時18分」だ。18時になっても動かないので待ち続け、翌朝ログを見て深夜0:18に動いていたことに気づいた。丸一日無駄にした。
正しくはこう。
0 18 * * * /home/user/scripts/check-jobs.sh
わかってしまえば当たり前なんだけど、時刻を「18:00」と頭の中で読んでいると、そのままの順番で書いてしまう。時計の読み順とcronの書式が逆なのが罠だと思う。
この手の書式ミスは、書いた式を実行前に検証すれば防げる。自分は懲りたので、cron式を貼ると日本語の解説と次回実行日時5回分を表示してくれるツールを作った。**cron式ジェネレータ**に18 0 * * *を貼ると、次回実行が「明日の0:18」と出るので、その場で「あれ、逆だ」と気づける。設定してから丸一日待つより、ずっと早い。
ハマり2: 曜日の0と7、そして日と曜日のOR問題
次に「日曜だけ集計を回す」設定を追加したときの話。曜日フィールドは0〜6で日曜が0……と覚えていたが、実は0と7の両方が日曜を意味する(多くの実装で7も受け付ける)。
0 9 * * 0 # 日曜9:00
0 9 * * 7 # これも日曜9:00(同じ意味)
これ自体は互換のための仕様なので実害はない。実害があったのはもうひとつの方で、「日」と「曜日」を両方指定するとANDではなくORになるという仕様だ。
0 9 1 * 1 # 「毎月1日かつ月曜の9:00」ではない
これは「毎月1日の9:00」と「毎週月曜の9:00」の両方で実行される。月に4〜5回余計に動く。「毎月1日が月曜のときだけ」をやりたいなら、cron側では素直に書けないので、スクリプト側でdate +%uを見て曜日判定するのが定石だ。
0 9 1 * * [ "$(date +\%u)" = "1" ] && /home/user/scripts/monthly.sh
ちなみにcrontab内の%は改行扱いされるのでエスケープが要る。これも地味な罠。
ハマり3: cron環境のPATHが最小限で、nodeが見つからない
書式が正しくなっても、まだ動かなかった。手動で./check-jobs.shを叩くと動くのに、cron経由だと何も起きない。
原因はPATHだった。cronが実行するときの環境変数は、普段のシェルとはまったく別物で、PATHは/usr/bin:/bin程度しか入っていない。自分はnodeをnvmで入れていたので、実体は~/.nvm/versions/node/v22.x/bin/nodeにある。cronから見るとnodeというコマンドは存在しない。
which nodeで実体のパスを調べて、対処は2通り。
# 方法1: crontabの先頭でPATHを宣言する
PATH=/home/user/.nvm/versions/node/v22.11.0/bin:/usr/local/bin:/usr/bin:/bin
0 18 * * * cd /home/user/app && node scripts/check-jobs.js
# 方法2: コマンドを絶対パスで書く
0 18 * * * cd /home/user/app && /home/user/.nvm/versions/node/v22.11.0/bin/node scripts/check-jobs.js
自分は方法1にした。行が短く保てるし、スクリプトが増えても1箇所で済む。あわせてcdを入れているのにも理由があって、cronの作業ディレクトリはホームディレクトリになるため、相対パスで.envや設定ファイルを読むスクリプトはcdしないと壊れる。「手動だと動くのにcronだと動かない」系は、PATHか作業ディレクトリを疑えばだいたい当たる。
ログをファイルに残す。これを最初にやるべきだった
3つのハマりを振り返ると、共通する問題は「何が起きたか見えない」ことだった。なので今は、cronに登録するコマンドには必ずログのリダイレクトをつけている。
0 18 * * * cd /home/user/app && node scripts/check-jobs.js >> /home/user/logs/check-jobs.log 2>&1
>>で追記、2>&1で標準エラーも同じファイルへ。これがあれば「実行されたのか」「エラーで死んだのか」がtail一発でわかる。nodeが見つからない問題も、ログさえあればnode: command not foundと一行出て終わりだった。切り分けに使った時間の大半は、ログがなかったせいだ。
追記し続けると太るので、ログローテートを組むか、雑にやるなら日付入りファイルにする手もある。個人のVPSなら月1で消すくらいの運用でも困っていない。
最終形のcrontab
いま実際に動いている形に近いものを置いておく。
PATH=/home/user/.nvm/versions/node/v22.11.0/bin:/usr/local/bin:/usr/bin:/bin
# 毎日18:00 案件チェック
0 18 * * * cd /home/user/app && node scripts/check-jobs.js >> /home/user/logs/check-jobs.log 2>&1
# 毎週日曜9:00 週次集計
0 9 * * 0 cd /home/user/app && node scripts/weekly-report.js >> /home/user/logs/weekly.log 2>&1
ポイントは3つ。書式は「分 時 日 月 曜日」の順、PATHと作業ディレクトリはcron側で明示、ログは必ずファイルに残す。この3つを押さえてからは、cronで詰まった記憶がない。
cron式そのものは、正直なところ毎回書いていると忘れる。「平日だけ」「2時間おき」あたりは書けても、次に書くのは数ヶ月後だったりするからだ。そのために作った**cron式ジェネレータ**は、フォームで選ぶだけで式を組み立てられて、逆に既存の式を貼れば日本語解説と次回実行日時が出る。crontab書式のチートシートも同じページに置いたので、自分は今後cronを書くたびにここを開くことになると思う。設定してから翌日まで待って「動かない」とやるのは、もう卒業したい。