
crontabとは、cronデーモンに「いつ・どのコマンドを実行するか」を1行1ジョブで登録する表と、その表を編集するcrontabコマンドの両方を指す名前です。Ubuntu 26.04.1 の cron 3.0pl1 を検証コンテナで直接動かし、書いたはずのジョブが動かない典型パターンを5つ実際に起こして、ログに何が出るかを記録しました。
対象は、バックアップやログ整理をcronに任せているインフラ担当・情シスの方と、自宅サーバーで定期処理を組み始めた方です。読み終えると、5つのフィールドの書き方に加えて、「ターミナルでは動くのにcronだと動かない」ときにどのログのどの行を見ればよいか、多重起動をflockで止める書き方、そしてcronを捨ててsystemd timerに移すべき条件が手元に残ります。
この記事でわかること
- 5つの時刻フィールドの書き方と、ユーザー用crontab・
/etc/cron.dの書式の違い - cronのPATHが
/usr/bin:/binしかないこと、%でコマンドが途中で切れることなど、動かない原因ごとの実際のログ flock -nで長時間ジョブの重複実行を止めた結果(ロックなしは3本同時に走った)- cronとsystemd timerの判断表と、
systemd-analyze calendarで時刻指定を事前確認する方法
検証環境:cronをコンテナで直接起動する
検証はDockerのubuntu:26.04コンテナ(ホスト名 nagoyadevlab01、作業ユーザー ryoma)で行いました。どちらも検証用に付けた名前なので、コマンドやログに出てくるryoma、/home/ryoma、nagoyadevlab01は自分のユーザー名とホスト名に読み替えてください。コンテナ内ではsystemdを起動しないので、cronとrsyslogはサービス経由ではなくデーモンを直接フォアグラウンドで動かしています。cronのログはsyslogのcronファシリティに出るため、ログを/var/log/syslogで読めるようにrsyslogも入れました。コンテナでは読めないカーネルログの入力(imklog)だけ無効にしています。
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq cron sudo systemd tzdata >/dev/null 2>&1; echo "exit=$?"
exit=0
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq rsyslog >/dev/null 2>&1; dpkg-query -W rsyslog
rsyslog 8.2512.0-1ubuntu4.2
grep PRETTY_NAME /etc/os-release; dpkg-query -W cron systemd util-linux
PRETTY_NAME="Ubuntu 26.04.1 LTS"
cron 3.0pl1-200ubuntu1
systemd 259.5-0ubuntu3.4
util-linux 2.41.3-3ubuntu2.2
useradd -m -s /bin/bash ryoma && echo "ryoma ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ryoma && chmod 440 /etc/sudoers.d/ryoma && id ryoma
uid=1001(ryoma) gid=1001(ryoma) groups=1001(ryoma)
ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && echo Asia/Tokyo > /etc/timezone && date
Tue Sep 29 16:51:55 JST 2026続けて/etc/rsyslog.confのmodule(load="imklog" …)行を先頭に#を付けてコメントアウトし、2つのデーモンをそれぞれ別の端末(またはdocker exec -d)で起動します。cron -fの-fはフォアグラウンド動作の指定で、コンテナのようにサービス管理がない環境ではこれがいちばん素直です。実機のUbuntuサーバーではcronはcron.serviceとして最初から動いているので、この手順は不要です。
grep -n "^#module(load=\"imklog\"" /etc/rsyslog.conf
25:#module(load="imklog" permitnonkernelfacility="on")
rsyslogd -n
cron -f
pgrep -a cron; pgrep -a rsyslogd
3199 cron -f
3175 rsyslogd -nもう1つ前提として、検証用に/usr/local/bin/disk-reportという小さなスクリプトを置きました。df -h /の結果を1行出すだけですが、置き場所が/usr/local/binであることが後で効いてきます。
printf "#!/bin/sh\necho \"disk-report: \$(df -h / | tail -1)\"\n" > /usr/local/bin/disk-report && chmod 755 /usr/local/bin/disk-report && cat /usr/local/bin/disk-report
#!/bin/sh
echo "disk-report: $(df -h / | tail -1)"crontabの書き方:5つの時刻フィールドと2種類のファイル
1行の書式は「分 時 日 月 曜日 コマンド」の順に空白で区切ります。値の範囲は分が0〜59、時が0〜23、日が1〜31、月が1〜12、曜日が0〜7(0と7はどちらも日曜)です。範囲外の値は登録時点で弾かれます。登録時の実際のエラーは、次の節で最初のcrontabを入れた直後に示します。
よく使う書き方を表にまとめます。時刻はcronが動いているマシンのタイムゾーン(ここでは/etc/localtimeをAsia/Tokyoにしたので日本時間)で解釈されます。クラウドのイメージはUTCのままのことが多く、「毎日2時30分」が日本時間の11時30分に走っていた、という取り違えはタイムゾーンを先にdateで確かめれば防げます。
| 書き方(例) | 意味 | 補足 |
|---|---|---|
30 2 * * * | 毎日2時30分 | 最も多い形。分を0にしない方がほかのジョブと重なりにくい |
*/15 * * * * | 15分ごと(0, 15, 30, 45分) | 「起動から15分おき」ではなく、時計の0分基準 |
30 2 * * 1-5 | 平日の2時30分 | 曜日は1が月曜、5が金曜 |
0 9 1,15 * * | 毎月1日と15日の9時 | カンマで列挙 |
0 3 1-7 * 0 | 1〜7日または日曜の3時 | 「第1日曜」にはならない(下の注意) |
@reboot | cron起動時に1回 | 起動直後のログにRunning @reboot jobsが出る |
日と曜日の両方に値を書くと、cronは「どちらかが一致した日」に実行します(crontab(5)の仕様)。0 3 1-7 * 0は第1日曜ではなく、毎月1〜7日の毎日と、すべての日曜に走ります。第1日曜にしたいなら0 3 1-7 * *にしてコマンド側で[ "$(date +\%u)" = 7 ] &&のように曜日を判定するか、後述のsystemd timerでSun *-*-1..7 03:00と書きます。timerは日付と曜日を両方満たす日だけを選びます。
ユーザーのcrontabと/etc/cron.dは書式が1列違う
ユーザーごとの表(crontabコマンドで登録するもの)は、実体が/var/spool/cron/crontabs/<ユーザー名>に置かれ、そのユーザーの権限で実行されます。ファイルのモードは600、グループはcrontabです。このファイルを直接エディタで書き換えると所有者やモードが崩れやすく、crontabコマンドの構文チェックも通らないので、編集は必ずcrontabコマンド経由で行います。
一方、/etc/crontabと/etc/cron.d/配下のファイルはシステム用で、時刻の5フィールドとコマンドの間に「実行ユーザー」の列が1つ増えます。パッケージが自分のジョブを配るのは/etc/cron.dで、Ansibleなどの構成管理で配布するのもこちらが向いています。ユーザーのcrontabと違ってcrontabコマンドの構文チェックを通らないため、間違いはログを見るまで表に出ません(後述の「つまずきポイント」で実際のエラーを示します)。
登録内容の確認・登録・削除:crontabコマンドの使い方
現在の内容はcrontab -lで確認します。まだ何も登録していないユーザーでは次のメッセージが出て、終了コードは1です。監視スクリプトでcrontab -lの終了コードを見ている場合、「未登録」も失敗として扱われる点に気をつけてください。
crontab -l
no crontab for ryoma対話的に編集するならcrontab -e(エディタは環境変数VISUAL/EDITORで決まります)ですが、筆者は手順書やサーバー間で同じ内容を入れる場面では、ファイルに書いてからcrontab ファイル名で丸ごと登録する方法を勧めます。ファイルをGitで管理でき、差分レビューもできるからです。ただしcrontab ファイル名は追記ではなく置き換えなので、既存の行があるサーバーでは先にcrontab -lの内容をファイルへ取り込んでから編集します。
mkdir -p ~/cron-lab && cd ~/cron-lab && printf "%s\n" "* * * * * env > /home/ryoma/cron-lab/cron-env.txt" > first.cron && cat first.cron
* * * * * env > /home/ryoma/cron-lab/cron-env.txt
crontab ~/cron-lab/first.cron && crontab -l
* * * * * env > /home/ryoma/cron-lab/cron-env.txt登録後にcronを再起動する必要はありません。cronは毎分crontabの更新を確認し、読み直したときにログへ(ユーザー名) RELOAD (crontabs/ユーザー名)の形の行を残します。変更したのに反映されないと感じたら、再起動の前にこのRELOAD行が出ているかを見ます。
登録した実体は/var/spool/cron/crontabs/にあり、先に述べたとおりモード600、グループcrontabになっています。
sudo ls -l /var/spool/cron/crontabs/
total 4
-rw------- 1 ryoma crontab 255 Sep 29 16:52 ryoma範囲外の値を書いたファイルは、この登録の段階で弾かれます。分に60を書いて試すと、crontabは行番号付きで拒否し、いま入れたfirst.cronの内容は書き換わりません。
printf "60 2 * * * disk-report\n" > ~/cron-lab/bad-minute.cron; crontab ~/cron-lab/bad-minute.cron
"/home/ryoma/cron-lab/bad-minute.cron":0: bad minute
errors in crontab file, can't install.次のcrontab -rを試す前に、~/cron-lab/cron-env.txtができるまで(時計の分が切り替わるまで)待ちます。このファイルは後の節でcronの実行環境を調べるのに使います。
crontab -rは確認なしでそのユーザーのcrontabを全部消します。キーボード上で-eの隣にあるため打ち間違えやすく、消した直後のcrontab -lは「no crontab for ユーザー名」を返すだけです。変更作業の前にはcrontab -l > ~/cron-lab/crontab-backup-$(date +%Y%m%d).txtのように日付付きで退避しておき、削除するときも-i(確認付き)を付ける習慣にしておくと取り返しがつきます。
crontab -l > ~/cron-lab/crontab-backup-$(date +%Y%m%d).txt && crontab -r && crontab -l
no crontab for ryoma
ls ~/cron-lab/crontab-backup-*
/home/ryoma/cron-lab/crontab-backup-20260929.txtつまずきポイント:cron設定が動かない5つの原因を再現する
「ターミナルで打つと動くのに、cronに書くと動かない」ときの原因は、ほぼ次の5つのどれかでした。調べる順番は、まずcronがその行を実行しようとしたかをログのCMD行で確かめ、出ていなければファイル側(書式・改行・ユーザー欄・ファイル名)、出ていれば実行時の環境(PATH・%・出力の行き先)を疑う、です。検証コンテナでは/var/log/syslogを読みましたが、rsyslogを入れていない実機ではjournaldに記録されるのでjournalctl -u cronで同じ行を探します(このjournalctlはsystemdを起動していない検証環境では実行していません)。
再現に使ったcrontabは次の3行です。1行目はリダイレクトなし、2行目は同じコマンドの出力をファイルへ、3行目は日付入りの文字列をファイルへ書きます。どれもログインシェルでは問題なく動く書き方です。
cat > ~/cron-lab/broken.cron <<'EOF'
# 分 時 日 月 曜日 コマンド
* * * * * disk-report
* * * * * disk-report >> /home/ryoma/cron-lab/disk.log 2>&1
* * * * * echo "stamp $(date +%Y-%m-%d)" >> /home/ryoma/cron-lab/stamp.log 2>&1
EOF
crontab ~/cron-lab/broken.cron && crontab -l
# 分 時 日 月 曜日 コマンド
* * * * * disk-report
* * * * * disk-report >> /home/ryoma/cron-lab/disk.log 2>&1
* * * * * echo "stamp $(date +%Y-%m-%d)" >> /home/ryoma/cron-lab/stamp.log 2>&11分後のログです。3行ともCMDが出ているのでcronは実行を試みていますが、3行目のコマンドがecho "stamp $(date +で途切れ、No MTA installed, discarding outputが2回出ています。以下、原因ごとに見ていきます。
sudo grep CRON /var/log/syslog | tail -n 8
2026-09-29T16:55:01.908639+09:00 nagoyadevlab01 CRON[3500]: (ryoma) CMD (disk-report >> /home/ryoma/cron-lab/disk.log 2>&1)
2026-09-29T16:55:01.910555+09:00 nagoyadevlab01 CRON[3501]: (ryoma) CMD (echo "stamp $(date +)
2026-09-29T16:55:01.910998+09:00 nagoyadevlab01 CRON[3499]: (ryoma) CMD (disk-report)
2026-09-29T16:55:01.921163+09:00 nagoyadevlab01 CRON[3493]: (CRON) info (No MTA installed, discarding output)
2026-09-29T16:55:01.922473+09:00 nagoyadevlab01 CRON[3495]: (CRON) info (No MTA installed, discarding output)PATHが/usr/bin:/binしかない
最初に登録したenv > …/cron-env.txtの結果と、ログインシェルのPATHを並べると差ははっきりします。ログインシェルには/usr/local/binや/usr/sbinが入っていますが、cronが渡す環境はPATH=/usr/bin:/bin、SHELL=/bin/shだけです。.bashrcも.profileも読まれません。
bash -lc 'echo "$PATH"'
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cat ~/cron-lab/cron-env.txt
HOME=/home/ryoma
LOGNAME=ryoma
PATH=/usr/bin:/bin
SHELL=/bin/sh
PWD=/home/ryomaそのため/usr/local/bin/disk-reportはcronからは見つかりません。出力をファイルに向けた2行目のおかげで、エラー文が残っていました。/usr/sbinにあるlogrotateをフルパスなしで呼んだ場合も同じ結果になります。
cat ~/cron-lab/disk.log
/bin/sh: 1: disk-report: not found直し方は2通りあります。表の先頭にPATH=/usr/local/bin:/usr/bin:/binを書くか、コマンドをフルパスで書くかです。筆者は両方やります。PATH行はcrontab全体に効くので、あとから行を足した人がフルパスを忘れても動き、フルパスは読んだ人に「どのバイナリか」を迷わせないからです。
シェルの感覚でPATH=$PATH:/usr/local/binと書くのは逆効果です。環境変数行では$PATHが展開されず、文字列がそのままPATHになるため、/usr/binにあるenvすら見つからなくなりました。
printf "%s\n" "PATH=\$PATH:/usr/local/bin" "* * * * * env > /home/ryoma/cron-lab/cron-env2.txt 2>&1" > ~/cron-lab/pathvar.cron && crontab ~/cron-lab/pathvar.cron && crontab -l
PATH=$PATH:/usr/local/bin
* * * * * env > /home/ryoma/cron-lab/cron-env2.txt 2>&1
cat ~/cron-lab/cron-env2.txt
/bin/sh: 1: env: not found出力がメールに回されて消えている
cronはジョブの標準出力と標準エラーを、crontabの持ち主宛てのメールで送ろうとします。メール配送エージェント(MTA)が入っていないサーバーでは(CRON) info (No MTA installed, discarding output)をログに残して捨てるだけです。1行目のdisk-reportも、実際には「not found」を出していたのに、その文字列はどこにも残りませんでした。「エラーが何も出ていないのに結果もない」ときは、まずこの行を探します。
対策は、すべての行を>> ログファイル 2>&1で終えることです。2>&1を忘れると標準エラーだけがメール側に回り、肝心のエラー文が消えます。メールが不要と決めたらMAILTO=""を書けば送信自体をやめます。逆に、Postfixなどが入っていて通知を受けたいなら[email protected]のように宛先を明示します(この環境ではメール送信は試していません)。
%がコマンドを途中で切る
コマンド欄では、エスケープしていない%は改行として扱われ、最初の%から後ろはコマンドの標準入力に回されます。ログのCMD (echo "stamp $(date +)がまさにその切れ端で、シェルに渡ったのは閉じていない$(を含む断片です。同じ断片を手でshに渡すと、cronの裏で起きていたエラーが見えます。
sh -c 'echo "stamp $(date +'
sh: 1: Syntax error: end of file unexpected (expecting ")")シェルが構文エラーで止まるので、行末の>> stamp.log 2>&1も解釈されず、ログファイルには何も残りません(この回のエラーもメール行きになり捨てられました)。date +%Fやファイル名の日付、printfの書式など、%を使うところはすべて\%と書きます。長くなるならスクリプトファイルに切り出し、crontabからはそのスクリプトを呼ぶだけにすると、この問題自体がなくなります。
PATH・出力・%の3つを直したcrontabを入れ直し、1分待った結果です。stamp.logに日付が入り、disk.logの2行目でdisk-reportが動いたことがわかります(1行目は修正前の実行分です)。
cat > ~/cron-lab/fixed.cron <<'EOF'
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
# 分 時 日 月 曜日 コマンド
* * * * * disk-report >> /home/ryoma/cron-lab/disk.log 2>&1
* * * * * echo "stamp $(date +\%Y-\%m-\%d)" >> /home/ryoma/cron-lab/stamp.log 2>&1
EOF
crontab ~/cron-lab/fixed.cron && crontab -l
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
# 分 時 日 月 曜日 コマンド
* * * * * disk-report >> /home/ryoma/cron-lab/disk.log 2>&1
* * * * * echo "stamp $(date +\%Y-\%m-\%d)" >> /home/ryoma/cron-lab/stamp.log 2>&1
cat ~/cron-lab/stamp.log; tail -n 2 ~/cron-lab/disk.log
stamp 2026-09-29
/bin/sh: 1: disk-report: not found
disk-report: overlay 150G 13G 130G 9% /最後の行に改行がない
各行は改行で終わっている必要があります。printfやテンプレートエンジンで生成したファイルは末尾の改行が抜けやすく、crontabコマンドで登録しようとすると次のように拒否されます。ここで気づけるなら害はありません。
printf "*/5 * * * * disk-report >> /home/ryoma/cron-lab/disk.log 2>&1" > ~/cron-lab/nonl.cron; crontab ~/cron-lab/nonl.cron
new crontab file is missing newline before EOF, can't install.厄介なのは/etc/cron.dに直接置いた場合です。こちらは登録コマンドを通らないので、ファイルは置けてしまい、cronが読み込む時点でファイル全体を無視します。コマンドの画面には何も出ず、ログにERROR (Missing newline before EOF, this crontab file will be ignored)が1行出るだけです。
/etc/cron.dのユーザー欄とファイル名
/etc/cron.dの3つの落とし穴を一度に試しました。ユーザー欄を書き忘れたファイル、ファイル名に.を含むファイル、末尾改行のないファイル、そして正しいファイルの4つを置いています。
sudo tee /etc/cron.d/disk-report-nouser >/dev/null <<'EOF'
PATH=/usr/local/bin:/usr/bin:/bin
* * * * * disk-report >> /tmp/nouser.log 2>&1
EOF
printf "PATH=/usr/local/bin:/usr/bin:/bin\n* * * * * ryoma disk-report >> /tmp/dot.log 2>&1\n" | sudo tee /etc/cron.d/disk.report >/dev/null
printf "PATH=/usr/local/bin:/usr/bin:/bin\n* * * * * ryoma disk-report >> /tmp/nonl.log 2>&1" | sudo tee /etc/cron.d/disk-report-nonl >/dev/null
sudo tee /etc/cron.d/disk-report >/dev/null <<'EOF'
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
* * * * * ryoma disk-report >> /home/ryoma/cron-lab/crond.log 2>&1
EOF
ls -l /etc/cron.d/
total 20
-rw-r--r-- 1 root root 111 Sep 29 16:56 disk-report
-rw-r--r-- 1 root root 83 Sep 29 16:56 disk-report-nonl
-rw-r--r-- 1 root root 80 Sep 29 16:56 disk-report-nouser
-rw-r--r-- 1 root root 83 Sep 29 16:56 disk.report
-rw-r--r-- 1 root root 188 Feb 13 2026 e2scrub_all次の分のログでは、改行なしのファイルが無視され、ユーザー欄のないファイルはdisk-reportをユーザー名として読んだ結果bad usernameで丸ごと無視されました。実行されたのは正しいファイルの1行だけです。そしてdisk.reportについては、エラーも実行記録も一切出ていません。
sudo grep -E "cron\[|CRON\[" /var/log/syslog | tail -n 8
2026-09-29T16:57:01.942439+09:00 nagoyadevlab01 cron[3199]: (*system*disk-report-nonl) ERROR (Missing newline before EOF, this crontab file will be ignored)
2026-09-29T16:57:01.942634+09:00 nagoyadevlab01 cron[3199]: Error: bad username; while reading /etc/cron.d/disk-report-nouser
2026-09-29T16:57:01.942646+09:00 nagoyadevlab01 cron[3199]: (*system*disk-report-nouser) ERROR (Syntax error, this crontab file will be ignored)
2026-09-29T16:57:01.948707+09:00 nagoyadevlab01 CRON[3919]: (ryoma) CMD (disk-report >> /home/ryoma/cron-lab/crond.log 2>&1)
ls /tmp/*.log; cat ~/cron-lab/crond.log
/tmp/evidence-bg-68353495.log
/tmp/evidence-bg-68356762.log
disk-report: overlay 150G 13G 130G 9% /Debian/Ubuntuのcronは、/etc/cron.dのファイル名に.が入っていると(backup.cronやdisk.reportなど)黙って読み飛ばします。上の検証でも/tmp/dot.logは作られず、ログにも何も出ませんでした。ファイル名は英数字・-・_だけにします。エラーが出ないぶん、5つの原因の中でいちばん気づきにくいものです。/tmpの2つのログファイルは検証ツールが作ったもので、cronとは関係ありません。
flockで多重起動を防ぐ
cronは前回の実行が終わったかどうかを気にしません。間隔より長くかかるジョブは、そのまま重なって走ります。150秒かかる処理を毎分起動する設定を、ロックなしとロックありで同時に登録して比べました。ロックにはutil-linuxのflockを使います。-nはロックが取れなければ待たずに失敗する指定で、-E 75はそのときの終了コードを既定の1から75に変え、「ロックで見送った」のか「ジョブ自体が失敗した」のかを区別できるようにしています。
sudo tee /usr/local/bin/slow-job >/dev/null <<'EOF'
#!/bin/sh
# 集計バッチの代わり: 150秒かかる処理
echo "$(date +%T) start pid=$$"
sleep 150
echo "$(date +%T) end pid=$$"
EOF
sudo chmod 755 /usr/local/bin/slow-job
sudo tee /etc/cron.d/slow-job >/dev/null <<'EOF'
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
* * * * * ryoma slow-job >> /home/ryoma/cron-lab/nolock.log 2>&1
* * * * * ryoma flock -n -E 75 /run/lock/slow-job.lock slow-job >> /home/ryoma/cron-lab/locked.log 2>&1 || echo "$(date +\%T) skipped rc=$?" >> /home/ryoma/cron-lab/locked.log
EOF
cat /etc/cron.d/slow-job
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
* * * * * ryoma slow-job >> /home/ryoma/cron-lab/nolock.log 2>&1
* * * * * ryoma flock -n -E 75 /run/lock/slow-job.lock slow-job >> /home/ryoma/cron-lab/locked.log 2>&1 || echo "$(date +\%T) skipped rc=$?" >> /home/ryoma/cron-lab/locked.logスクリプトファイルの中の%Tはエスケープ不要で、crontabの行に書いた\%Tだけがエスケープ対象です。3回目の起動直後にプロセスを見ると、ロックなし側は3本(pid 4206・4302・4398)が同時に動き、ロックあり側は最初の1本(pid 4205)だけでした。
pgrep -af "slow-job$"
4204 flock -n -E 75 /run/lock/slow-job.lock slow-job
4205 /bin/sh /usr/local/bin/slow-job
4206 /bin/sh /usr/local/bin/slow-job
4302 /bin/sh /usr/local/bin/slow-job
4398 /bin/sh /usr/local/bin/slow-job
cat ~/cron-lab/nolock.log; echo ---; cat ~/cron-lab/locked.log
16:58:01 start pid=4206
16:59:01 start pid=4302
17:00:01 start pid=4398
---
16:58:01 start pid=4205
16:59:01 skipped rc=75
17:00:01 skipped rc=75ロックあり側は、最初の実行が終わった次の分から再び動き始めます。ロックファイルはプロセスが終了した時点で自動的に解放されるので、ジョブが異常終了しても「ロックが残って二度と動かない」状態にはなりません。pidファイルを自前で作って存在チェックする方式よりflockを勧める理由はここにあります。
cat ~/cron-lab/locked.log
16:58:01 start pid=4205
16:59:01 skipped rc=75
17:00:01 skipped rc=75
17:00:31 end pid=4205
17:01:02 start pid=4538見送りを許せないジョブ(取りこぼすと集計がずれるものなど)では-nを外して待たせる手もありますが、待ち行列が伸び続けるだけなので、筆者は間隔を広げるか、処理時間を先に短くする方を選びます。
cronとsystemd timerの使い分け
systemd timerは、.timerユニットが時刻を決め、同じ名前の.serviceユニットを起動する仕組みです。ここまでの5つの落とし穴のうち、PATHはExecStart=にコマンドを絶対パスで書けば左右されず、出力は標準でjournalに入るため、そもそも起きにくくなります。多重起動も、同じサービスが実行中なら次の起動は重ならない作りです。その代わりファイルが2つに増え、systemctl daemon-reloadとenableの手順が加わります。
| 判断軸 | cron(crontab / cron.d) | systemd timer |
|---|---|---|
| 電源オフ中に過ぎた時刻 | 実行されない | Persistent=trueで起動後に1回実行 |
| 実行ログ | 自分でリダイレクトを書く。忘れるとMTA行き | journalに自動で入る(journalctl -u) |
| 多重起動 | 自分でflockを書く | 実行中のサービスは二重起動しない |
| メモリ・CPU制限 | なし(nice程度) | サービス側でMemoryMax=などを指定できる |
| 時刻指定の事前確認 | 手段なし(待つしかない) | systemd-analyze calendarで次回時刻を表示 |
| 記述量・一般ユーザーの扱いやすさ | 1行で済む。crontab -eだけで完結 | 2ファイル+root権限(ユーザー単位のtimerもあるが要設定) |
| コンテナ・systemdのない環境 | そのまま動く | 使えない |
筆者の判断基準はこうです。ノートPCや夜間に止める検証機のように電源が落ちる可能性があるマシン、実行時間が読めず重なる可能性があるジョブ、失敗を監視に拾わせたいジョブはsystemd timerにします。逆に、一般ユーザーが自分の作業だけを数行登録する場合、systemdのないコンテナ、他社製品が/etc/cron.dに配るジョブはcronのままで構いません。既存のcronジョブを全部移す必要はなく、トラブルが起きたジョブから移すのが現実的です。
timerとserviceを書いてsystemd-analyzeで確認する
disk-reportを平日2時30分に動かすユニットを書き、systemd-analyze verifyで文法を確かめました。Type=oneshotは「起動して終わるまでを1回の実行とみなす」指定、Persistent=trueは止まっていた間に過ぎた実行を起動後に1回行う指定、RandomizedDelaySec=5minは複数台が同時刻に一斉に動かないよう0〜5分の範囲で遅らせる指定です。
sudo tee /etc/systemd/system/disk-report.service >/dev/null <<'EOF'
[Unit]
Description=Disk usage report
[Service]
Type=oneshot
User=ryoma
ExecStart=/usr/local/bin/disk-report
EOF
sudo tee /etc/systemd/system/disk-report.timer >/dev/null <<'EOF'
[Unit]
Description=Run disk-report on weekdays at 02:30
[Timer]
OnCalendar=Mon..Fri *-*-* 02:30:00
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
EOF
systemd-analyze verify /etc/systemd/system/disk-report.timer /etc/systemd/system/disk-report.service; echo "verify exit=$?"
verify exit=0OnCalendar=の式はsystemd-analyze calendarに渡すと、次に何時に動くかを実際の日付で返します。cronでは登録してから待つしかなかった「この書き方で本当に平日だけか」を、登録前に確かめられるのがtimerのいちばんの利点だと筆者は考えています。--iterationsで先の回数分を並べられます。
systemd-analyze --version | head -n 1
systemd 259 (259.5-0ubuntu3.4)
systemd-analyze calendar --iterations=3 "Mon..Fri *-*-* 02:30:00"
Normalized form: Mon..Fri *-*-* 02:30:00
Next elapse: Wed 2026-09-30 02:30:00 JST
(in UTC): Tue 2026-09-29 17:30:00 UTC
From now: 9h left
Iteration #2: Thu 2026-10-01 02:30:00 JST
(in UTC): Wed 2026-09-30 17:30:00 UTC
From now: 1 day 9h left
Iteration #3: Fri 2026-10-02 02:30:00 JST
(in UTC): Thu 2026-10-01 17:30:00 UTC
From now: 2 days leftcronの*/15に相当する書き方と、cronでは素直に書けなかった「第1日曜の3時」も確かめました。Sun *-*-1..7 03:00の次回は2026年10月4日(日)で、1〜7日のうち日曜の日だけが選ばれています。
systemd-analyze calendar --iterations=4 "*:0/15"
Original form: *:0/15
Normalized form: *-*-* *:00/15:00
Next elapse: Tue 2026-09-29 17:00:00 JST
(in UTC): Tue 2026-09-29 08:00:00 UTC
From now: 2min 32s left
Iteration #2: Tue 2026-09-29 17:15:00 JST
systemd-analyze calendar "Sun *-*-1..7 03:00"
Original form: Sun *-*-1..7 03:00
Normalized form: Sun *-*-01..07 03:00:00
Next elapse: Sun 2026-10-04 03:00:00 JST
(in UTC): Sat 2026-10-03 18:00:00 UTC
From now: 4 days lefttimerにも固有のつまずきがあります。キー名をOnCalender=と打ち間違えると、systemdは不明なキーを警告して無視し、時刻指定のないタイマーとして読み込みを拒否します。時刻として不正な値はcalendarの段階で弾かれます。どちらもenableする前に見つけられます。
sed "s/^OnCalendar=/OnCalender=/" /etc/systemd/system/disk-report.timer > /tmp/typo.timer && sudo cp /tmp/typo.timer /etc/systemd/system/disk-typo.timer && sudo cp /etc/systemd/system/disk-report.service /etc/systemd/system/disk-typo.service && systemd-analyze verify /etc/systemd/system/disk-typo.timer; echo "verify exit=$?"
/etc/systemd/system/disk-typo.timer:5: Unknown key 'OnCalender' in section [Timer], ignoring.
disk-typo.timer: Timer unit lacks value setting. Refusing.
Unit disk-typo.timer has a bad unit file setting.
verify exit=1
systemd-analyze calendar "Mon..Fri 25:00"; echo "exit=$?"
Failed to parse calendar specification 'Mon..Fri 25:00': Invalid argument
exit=1ここから先の有効化は、systemdがPID 1として動いている実機で行う手順です。検証コンテナではsystemdを起動していないため、次のブロックは実行しておらず、タイマーが実際に2時30分に発火する様子も観察していません。list-timersのNEXT列が上のcalendarの結果(ランダム遅延ぶん後ろにずれることがあります)と合っているかを見てください。
sudo systemctl daemon-reload
sudo systemctl enable --now disk-report.timer
systemctl list-timers disk-report.timer
journalctl -u disk-report.service -n 20動作確認
cronのジョブを新しく入れたら、次の3段で確かめます。デーモンが動いているか、cronがその行を実行したか(CMD行)、ジョブの出力先に期待した結果があるか、です。ログにCMDが出ていて出力ファイルが空なら実行時の環境、CMD自体がなければファイル側の問題、と切り分けられます。下の出力は、修正版のfixed.cronを登録した直後(/etc/cron.dとflockの検証より前)に採ったものです。手順を最後まで進めたあとに実行すると、pgrepには実行中ジョブの子プロセスの行が増え、tailにはより新しいCMD行が並びます。
pgrep -a cron; pgrep -a rsyslogd
3199 cron -f
3175 rsyslogd -n
sudo grep CRON /var/log/syslog | tail -n 3
2026-09-29T16:55:01.922473+09:00 nagoyadevlab01 CRON[3495]: (CRON) info (No MTA installed, discarding output)
2026-09-29T16:56:01.932222+09:00 nagoyadevlab01 CRON[3704]: (ryoma) CMD (echo "stamp $(date +%Y-%m-%d)" >> /home/ryoma/cron-lab/stamp.log 2>&1)
2026-09-29T16:56:01.932623+09:00 nagoyadevlab01 CRON[3705]: (ryoma) CMD (disk-report >> /home/ryoma/cron-lab/disk.log 2>&1)
grep '^PATH' ~/cron-lab/cron-env.txt
PATH=/usr/bin:/bin修正後のCMD行では、\%で書いた部分が%Y-%m-%dとしてシェルに渡っていることが読み取れます。本番サーバーで試すときは、いきなり本来の時刻で待たずに、いったん* * * * *(毎分)で登録して次の分のCMD行と出力を確認し、それから本来の時刻に書き換えると確認のたびに翌日まで待たずに済みます。
検証で置いた/etc/cron.d/配下のファイル(slow-job、disk-reportで始まるもの、disk.report)とユーザーのcrontabは、放っておくと毎分動き続けます。検証用コンテナごと破棄するか、ファイルを削除してcrontabも消しておきます。
よくある質問
cronとcrontabの違いは何ですか?
cronは常駐して毎分スケジュールを確かめ、ジョブを起動するデーモン(Ubuntuではcronパッケージ)です。一方のcrontabは、そのスケジュールを書いた表と、表を編集するcrontabコマンドを指します。ユーザーの表は/var/spool/cron/crontabs/に置かれ、編集は必ずcrontab -eかcrontab ファイル名で行います。
cronの代わりになるものは?
systemdが動くLinuxならsystemd timerが第一候補です。止まっていた間の実行をPersistent=trueで拾え、ログはjournalに入り、systemd-analyze calendarで次回時刻を事前に確認できます。一方、systemdのないコンテナや一般ユーザーが数行だけ登録する用途ではcronの方が手軽です。
crontabが実行できないのはなぜですか?
多いのは、PATHが/usr/bin:/binだけで/usr/local/binのコマンドが見つからない、%をエスケープしておらずコマンドが途中で切れる、出力がメールに回りMTAがなく捨てられている、の3つです。/etc/cron.dならユーザー欄の漏れ、ファイル名の.、末尾改行も疑ってください。まずログのCMD行の有無を見ます。
cronは実行中のジョブを途中で終了しますか?
終了しません。cronは起動するだけで、実行時間の上限も前回の完了待ちもありません。今回150秒かかるジョブを毎分起動したところ、3本が同時に動きました。時間制限が必要ならコマンドをtimeout 10mで包み、重なりはflock -nで防ぎます。cronデーモン自体は常駐し続けます。
まとめ
書き方を、先頭にPATH=とMAILTO=""、各行の末尾に>> ログ 2>&1、%は\%、長いジョブはflock -nという型にそろえておけば、今回再現した原因の大半は起きません。/etc/cron.dに置くファイルはユーザー欄・ファイル名・末尾改行の3点を確認します。電源断や重なりが気になるジョブはsystemd timerへ移し、systemd-analyze calendarで時刻を確かめてから有効化してください。次の作業としては、今お使いのサーバーでcrontab -lとls /etc/cron.dを見て、リダイレクトのない行とファイル名に.を含むファイルがないかを洗い出すところから始めるのがおすすめです。
次に読む
- resticでLinuxサーバーをバックアップする(systemd timerで毎日のバックアップを回す実例)
- ClamAVの使い方:Ubuntuサーバーで常駐スキャンと定期検査を動かす
- Linuxサーバーの基本ハードニング