SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-09-04

cronジョブが実行されない5つの原因と対処法

cronジョブが実行されない原因は5つです。最小限のPATH、未エスケープの%記号、誤ったcrontab、メールに消えた出力、シェル依存のスクリプトを確認します。

cron ジョブが実行されない理由

「一度も実行されない」cron ジョブも、実際にはほぼ必ず実行されています。シェルとは異なる環境で実行され、最初の 1 秒で失敗し、メッセージが確認していない場所に送られているだけです。ほとんどの事例は、検索パス、パーセント記号、誤った crontab ファイル、メールに送られた出力、ログインセッションを前提とするスクリプトの 5 つの原因で説明できます。

cron は crontab ファイルを読み込み、スケジュールに従ってコマンドを起動するデーモン(バックグラウンドサービス)です。cron は .bashrc を読み取らず、端末を開かず、ログインシェルを起動せず、コマンドが失敗したことも通知しません。以下の原因はすべて、この 4 つの事実から生じます。

以下の項目を順番に確認し、すべての項目に共通する次の質問から始めてください。cron は実際にジョブを起動したでしょうか。「cron がジョブを起動しなかった」ことと「ジョブが起動した後に終了した」ことは、共通点のない別の問題です。まずこの点を確認します。

cron は実行されたか

ディストリビューション系統によって、デーモンの unit 名が異なります。両方を確認してから、ログを読みます。

systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"

Debian と Ubuntu では unit を cron と呼びます。Fedora、Rocky、Alma では crond と呼びます。特定のマシンに存在する名前はどちらか一方だけです。そのため、2 つのコマンドの一方が unknown unit を報告しても正常であり、障害ではありません。

自分のシステムが実際に記録したエントリを読みます。ガイドから抜き出した行を探してはいけません。cron の実装やログの構成によって、メッセージの文言が異なるためです。確認するのは次の 2 点だけです。スケジュールで指定した分にエントリがあるか、そのエントリに自分のコマンドが記載されているかです。自分のコマンドを記載したエントリがあれば、cron の処理は完了しており、失敗箇所はコマンド内部です。エントリがまったくなければ、cron はスケジュールを取得していません。これは以下の原因 3 に該当します。

一部のイメージでは、cron のメッセージが rsyslog 経由でファイルに記録され、journal に送られません。/var/log で cron または syslog にちなんだ名前のファイルを探し、その末尾を読みます。

ls -l /var/log
sudo tail -n 50 /var/log/syslog

unit もログも存在しない場合、cron 自体がインストールされていない可能性があります。最小構成のクラウドイメージやコンテナでは、cron が含まれていないことがよくあります。

dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron

原因 1: cron に PATH が設定されていない

対話型シェルは、/etc/profile、~/.profile、~/.bashrc およびそれらのファイルから読み込まれる設定から PATH を構成します。cron ジョブでは、これらは実行されません。cron は独自の短い環境でコマンドを起動するため、標準のシステムディレクトリ以外にあるプログラムは見つかりません。/usr/local/bin、/opt 配下のプログラム、言語のバージョンマネージャー、Python の仮想環境、Go のワークスペースなどが該当します。ジョブは 1 行目で失敗し、シェルは「not found」形式のエラーを書き込みます。正確な文言は、実行したシェルによって異なります。

ジョブで使用するすべてのコマンドの実際のパスを確認します。

command -v docker
command -v node
readlink -f "$(command -v node)"

その後、絶対パスをジョブに記述するか、crontab の先頭で PATH を 1 回設定します。

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool run

この一覧は、自分のマシンで echo "$PATH" を使用して取得し、対話型セッションでのみ存在する項目を削除します。ここで重要な点が 1 つあります。cron は、これらの代入行内の変数を展開しません。PATH=$PATH:/usr/local/bin にはリテラル文字列 $PATH:/usr/local/bin が格納されるため、ジョブの検索パスには使用可能なディレクトリが 1 つも含まれなくなります。一覧全体を展開して記述してください。

バージョンマネージャーには、パスを設定するだけでは不十分です。nvm、pyenv、rbenv、asdf は、.bashrc から読み込まれるシェル関数または shims ディレクトリを設定しますが、cron ジョブはそのファイルを読みません。バージョン付きバイナリを絶対パスで呼び出すか、自分のスクリプトの 1 行目でバージョンマネージャーの init スクリプトを読み込んでください。

原因 2: パーセント記号でコマンドが終了します

crontab の command フィールドでは、% は通常の文字ではありません。エスケープされていない最初の % でコマンドが終了します。それ以降のすべての文字は、標準入力としてコマンドに渡され、続く各 % は改行になります。これは、短い入力をプログラムに渡すための cron の実際の機能です。同時に、日付入りのファイル名が crontab でよく壊れる原因でもあります。

0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site と書くと、tar は整形済みの日付を受け取りません。cron は最初の % で行を切るため、shell には未完了のコマンド置換が渡され、行の残りは標準入力として渡されます。すべてのパーセント記号をバックスラッシュでエスケープしてください。

0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site

1 行を順に読み取る層は 2 つあります。\% は cron の規則であり、cron が何かを起動する前に適用します。$(date +\%F) は コマンド置換であり、cron が起動した shell によって後から適用されます。各文字をどの層が処理するかを把握することが、問題を解くポイントです。

より安全な方法は、ロジックを crontab から完全に分離することです。ロジックをスクリプトに記述すれば、パーセント記号に特別な意味はありません。

#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site

その場合、crontab の行にはパスとリダイレクトだけを記述します。一目で読める crontab は、原因を切り分けやすい crontab です。

原因 3: どの crontab を編集しましたか?

crontab は 1 つではありません。所有者とフィールド数が異なる複数のファイルがあり、誤ったファイルに書いたジョブは認識されません。

  • crontab -e は、コマンドを実行したユーザーの crontab を編集します。sudo crontab -e は root の crontab を編集します。同じサーバーを 2 人で調査すると、別々のファイルを読んでいることがよくあります。
  • sudo crontab -l -u deploy は別のユーザーの crontab を表示します。ジョブを実行するアカウントに、実際にどの設定がインストールされているかを確認できます。
  • /etc/crontab と /etc/cron.d 内のすべてのファイルには、スケジュールとコマンドの間に追加のフィールドがあります。ここには実行ユーザーを指定します。5 フィールドのユーザー crontab の行を /etc/cron.d に貼り付けると、コマンドの最初の単語がユーザー名として解釈されます。
  • /etc/cron.d 内のファイル名には、英字、数字、アンダースコア、ハイフンだけを使用できます。backup.sh や site.conf という名前のファイルは、名前だけを理由にスキップされます。backup に名前を変更してから、ログをもう一度確認してください。
  • /etc/cron.d 内のファイルは root が所有し、group や others に書き込み権限を付与してはいけません。ls -l /etc/cron.d を使うと、この 2 つを同時に確認できます。
  • /etc/cron.daily とその兄弟ディレクトリに配置したスクリプトにも同じ命名規則が適用され、実行ビットも必要です。実行ビットがない場合、何も表示されずにスキップされます。
  • /etc/cron.allow と /etc/cron.deny により、誰が crontab をインストールできるかが決まります。サーバー上にどちらかが存在する場合は、ユーザーが crontab を使用できると判断する前に内容を確認してください。

ユーザーの crontab は、spool ファイルを手動で編集せず、crontab コマンドでインストールしてください。crontab はインストール前にファイルを解析するためです。保存時には、コマンドが返す内容を確認してください。ファイルが拒否されると、以前のバージョンが有効なままになり、変更は反映されません。これは cron が無視している場合とまったく同じように見えます。

所有者は権限も決定します。root の crontab に登録したジョブが作成するファイルは root 所有になり、それを読み取るアプリケーションから書き込めない場合があります。通常のユーザーの crontab に登録したジョブは、root 専用のディレクトリを読み取れません。作業内容に合わせて所有者を選んでください。アプリケーションの保守は、そのアプリケーション自身のアカウントで実行するのが適切です。これは、WordPress の wp-cron を system cron ジョブに置き換える方法で説明する考え方の根拠でもあります。ジョブが作成するファイルのモードは、引き継いだ umask で決まります。これはシェルの umask と異なる場合があるため、ジョブの出力が読み取れない場合は umask がファイル権限を設定する仕組みも確認してください。

原因 4: 出力が誰も読まないメールに送られていた

cron は、ジョブが標準出力と標準エラー出力に書き込んだ内容をすべて収集します。ジョブが何かを出力すると、cron はそのテキストをローカルのメールシステムに渡します。宛先は crontab の所有者、または MAILTO が指定する宛先です。最小構成の VPS には通常、MTA(メール転送エージェント)がインストールされていないため、メールは配信されません。エラーは一時的に存在した後、どこにも届かずに消えます。壊れたジョブが何も出力せずに失敗したように見える理由はこれです。

出力は、自分で管理できるファイルに送ってください。

0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

>> は標準出力をファイルに追記します。2>&1 は標準エラー出力を、その時点で標準出力が向いている先に送ります。そのため、リダイレクトの後に置く必要があります。逆の順序で 2>&1 >> file と書くと、標準エラー出力は元の出力先のままになり、調査中のエラーだけがファイルに届きません。

journal も適した出力先です。logger は、指定したタグを付けて syslog に書き込みます。

0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-site

journalctl -t backup-site で内容を読み取れます。これにより、ジョブ自身の出力を cron エントリの近くに記録できるため、時系列を追いやすくなります。どのユーザーがどのコマンドをサーバー上で実行したかも記録する必要がある場合、それは別の仕組みです。サーバー上のユーザーコマンドを監査するで説明しています。

crontab の先頭に MAILTO="" を置くと、その下にあるジョブのメール送信を無効にできます。MAILTO に実在するアドレスを設定しても、動作する MTA が存在する場合にしか役立ちません。メールがサーバーから送信されることを確認してから利用してください。

デバッグ中は、> /dev/null 2>&1 を決して追記しないでください。これはすべての crontab で最もよく使われる行ですが、唯一の証拠を破棄します。ジョブが動作することを確認した後で、必要なら元に戻してください。

原因 5: スクリプトが、cron から渡されない環境変数を前提にしている

コマンドが見つかり、その出力を取得できた後に残るのは、セッションが自動的に渡しているその他すべての要素です。

  • シェルは bash とは限りません。ls -l /bin/sh で確認します。Debian と Ubuntu では dash を指すため、二重角括弧のテスト、配列、source は構文エラーになります。スクリプトに #!/bin/bash 行を指定してスクリプトを呼び出すか、crontab の先頭で SHELL を設定します。
  • 作業ディレクトリは、手動実行時にいたディレクトリとは異なります。すべての場所で絶対パスを使用するか、スクリプトの 1 行目で cd によりディレクトリへ移動します。相対パスは、ジョブが「手動で実行すると動く」最大の原因です。
  • ロケールはセッションのロケールとは異なります。日付や数値の書式化、テキストのソートでは、異なる LANG の下で出力が変わることがあります。後続の処理でその出力を解析する場合は、期待するのではなく、スクリプト内でロケールを設定します。
  • TTY(端末)がありません。確認を求めるコマンド、エディターを開くコマンド、進捗バーを表示するコマンドは、停止したり終了したりすることがあります。ツールが提供する非対話モード用のフラグを追加します。
  • SSH agent がありません。SSH_AUTH_SOCK は cron の環境に含まれないため、agent が読み込まれていたことで動作していた ssh または rsync コマンドは認証に失敗します。ジョブ専用の鍵を用意し、ジョブのユーザーを所有者にします。
  • ユーザーセッションの bus がないため、cron ジョブからの systemctl --user は XDG_RUNTIME_DIR を設定するまで失敗します。system unit を使用する方が適切です。

Fedora、Rocky、Alma では、もう 1 つ確認すべき原因があります。SELinux は cron ジョブを制限するため、ファイル権限が正しく見えても、予期しないラベルのパスにアクセスするジョブは拒否されます。sudo ausearch -m avc -ts recent で拒否を確認し、設定を無効にする前に サーバー向け SELinux の基本 を読んでください。

cron の環境を 1 分で確認するプローブ

cron がどのような環境で実行するかを推測するのはやめて、実際に読み取ります。すべての情報を出力するスクリプトを作成し、毎分実行するように設定して待ちます。その後、ファイルを読み取ります。

cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.sh

実際のジョブを実行するユーザーの crontab に、両側で絶対パスを使用した行を 1 行追加します。

* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1

1 分待ってから /home/deploy/cron-probe.log を読み取り、自分のシェルで同じコマンドを実行した結果と比較します。PATH の行、作業ディレクトリ、ロケールを確認すると、通常はそれだけで失敗の原因が分かります。設定上の重要な点が 2 つあります。パーセント記号はスクリプト内にあるため、cron の規則は適用されません。また、ログのパスにはジョブの実行ユーザーが書き込めます。

原因が分かったら、直ちにその crontab の行を削除します。毎分実行してファイルに追記するジョブは、小容量のディスクを満杯にします。しかも、気付かないうちに実行し続けます。

指定したスケジュールは意図したものですか?

ユーザーの crontab の行は、minute、hour、day of month、month、day of week の5つのフィールドで始まります。このうち2つは、意外な動作をします。

day of month と day of week の両方を制限した場合、つまりどちらも * ではない場合、cron はどちらか一方のフィールドが一致したときにジョブを実行します。0 0 13 * 5 は「13日の金曜日」を意味しません。毎月13日の午前0時と、毎週金曜日の午前0時に実行されます。特定の日だけを指定するには、2つのフィールドの一方を * のままにし、もう一方の値をスクリプト内で判定します。

cron はシステムのタイムゾーンを使用します。多くの VPS イメージは UTC(協定世界時)に設定されているため、03:00 に設定したジョブが 03:00 UTC に実行され、現地では午後の時間帯になることがあります。timedatectl を実行すると、実際にシステムで使用されているタイムゾーンを確認できます。自分の環境を確認し、ラップトップと同じだと決めつけないでください。

スケジュールには、もう2つ注意すべき点があります。@reboot は cron 自体の起動時に実行されます。これはネットワークの準備が完了した時点とは異なるため、DNS やリモートホストを必要とするジョブは、ブート時に失敗し、その後の手動実行では毎回成功することがあります。また、時間のかかるジョブでは、前の実行がまだ終了していなくても次の実行が開始されます。ロックを使って重複実行を防いでください。

*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

flock -n はロックがすでに取得されている場合、直ちに終了します。そのため、重複した実行が最初の実行に重なることはありません。

systemd timer が適している場合

cron は、指定した時刻にコマンドを実行することには適しています。それ以外の機能は限られています。timer なら、リダイレクトなしで journal に記録でき、後から終了ステータスを確認できます。また、network-online.target との実行順序を指定でき、100 台のサーバーが同じ秒に一斉起動することを避けるランダムな遅延も設定できます。これらが必要なジョブでは、VPS 上で systemd service と timer を構成する方法のほうが、crontab の 1 行を維持するより手間がかかりません。service 側を記述すると、cron にはない点を 1 つ決める必要があります。それは、ユニットが処理の実際の開始をどのように認識するかです。そのため、まず simple、forking、notify の各ユニットにおける Type= の意味を確認してください。デフォルトのタイプでスクリプト自身を daemonize すると、実行対象が残っていないのにユニットが active のままになるためです。再試行の動作も service 側で設定します。障害後の動作は systemd の再起動ポリシーで決まりますが、cron にはこの点を制御する仕組みがありません。

小規模なジョブには cron を使い続けてください。依存関係や再試行ポリシーがある処理は timer に移します。両方を同じサーバーで実行できるため、この移行を 1 回で完了させる必要はありません。

FAQ

cron から実行すると失敗するのに、手動では成功するのはなぜですか?

シェルと cron の環境が異なるためです。ログインシェルは /etc/profile と ~/.bashrc を読み込み、PATH、ロケール、エージェント関連の変数を設定します。cron はそれらを何も設定せず、異なる作業ディレクトリから、場合によっては異なるシェルでコマンドを起動します。すべてのコマンドで絶対パスを使用し、必要な設定を crontab の先頭またはスクリプト内で指定してください。また、1 分後に実行する確認用ジョブを登録し、env | sort、pwd、id の結果をログファイルに書き出してください。推測ではなく、cron の実際の環境を確認できます。

cron が実際にジョブを実行したかどうかを確認するにはどうすればよいですか?

デーモンのログを確認します。Debian と Ubuntu では journalctl -u cron を使用し、Fedora、Rocky、Alma では journalctl -u crond を使用します。一部のイメージでは、メッセージが rsyslog 経由で /var/log 配下のファイルに書き込まれます。スケジュールで指定した時刻のエントリを探し、コマンド名が記録されていることを確認してください。エントリがなければ、cron はそのスケジュールを読み込んでいません。正しい crontab を編集したか確認してください。エントリがあって結果がない場合は、コマンドが起動後に終了しています。リダイレクトを使って出力を取得してください。

crontab 内で date +%Y を使うと失敗するのはなぜですか?

cron はコマンド欄の % を特殊文字として扱うためです。最初のエスケープされていない % でコマンドが終了し、それ以降はそのコマンドの標準入力に渡されます。さらに出現する % は改行になります。そのため、日付形式を含むファイル名が、意図したプログラムに渡りません。各パーセント記号を \% としてエスケープするか、コマンドをスクリプトに移して cron からスクリプトを呼び出してください。スクリプト内では、パーセント記号に特殊な意味はありません。

cron ジョブの出力先はどこですか?

ローカルのメールシステムに送られ、crontab の所有者、または MAILTO が指定する宛先に配信されます。多くの VPS イメージにはメール転送エージェントがインストールされていないため、メッセージは破棄され、ジョブが何も出力しなかったように見えます。>> /path/to/log 2>&1 を使って出力をファイルにリダイレクトしてください。標準エラーが標準出力に続くよう、この順序を維持します。または logger -t myjob を介して出力し、journalctl -t myjob で読み返してください。デバッグ中は > /dev/null 2>&1 を使用しないでください。

cron と systemd timer のどちらを使うべきですか?

固定時刻に単純なコマンドを実行する場合は cron を使用します。特に、systemd を実行しないマシンへ移行する可能性があるジョブに適しています。リダイレクトなしで journal に出力したい場合、終了ステータスを照会したい場合、ネットワーク起動後の順序を指定したい場合、開始時刻をランダムに遅延させたい場合、または失敗後の再試行ポリシーが必要な場合は timer を使用します。どちらも同じサーバーで実行できるため、必要になったジョブから 1 つずつ移行できます。