脚本手动跑得好好的,写进crontab就是不执行,或者执行了但结果是空的。这两种都遇到过,原因基本集中在环境变量、转义和crontab文件本身这三处,跟cron语法反而关系不大。
先确认一件事:cron到底有没有触发这条任务。没触发和触发了但脚本失败,排查方向完全不一样。
cron服务在不在跑
Debian、Ubuntu上服务名叫cron,RHEL、CentOS、Rocky上叫crond,名字不一样,systemctl查的时候别写错。发行差别只在包名和服务名:Debian 12、Ubuntu 24.04,装的是cron包;RHEL 9.4、9.5,装的是cronie,服务名一律crond。
systemctl status cron # Ubuntu 22.04、24.04 systemctl status crond # RHEL 8.10、9.4、CentOS 7.9 systemctl enable --now cron
容器里默认不装cron,也不跑systemd,docker镜像里指望cron生效的,得自己在entrypoint里拉起crond。
日志在哪
systemd的发行版直接看journal就行,比翻文件快:
journalctl -u cron --since "30 min ago" journalctl -u crond -f
RHEL家族有独立日志文件/var/log/cron;Debian、Ubuntu走syslog,装了rsyslog才有/var/log/syslog,没装就只剩journal。一条正常触发的记录长这样:
grep CRON /var/log/syslog | tail -20 # Sep 24 03:10:01 host CRON[28451]: (root) CMD (/opt/job/backup.sh)
能看到CMD那一行,说明cron触发了,问题在脚本里;连CMD都没有,问题在crontab文件本身。
crontab文件的几个硬规矩
1、文件末尾必须有换行。用echo追加生成的crontab,最后一行没有\n的话cron直接不认,日志里也不报错,纯靠肉眼很难发现:
crontab -l | tail -c 20 | xxd | tail -2 # 结尾看不到 0a 就是缺换行
2、百分号要转义。%在crontab里表示换行,命令里所有%都得写成\%。用date生成文件名的写法最容易中招:
# 错:cron会把 %F 当成换行,命令被截断 0 3 * * * cp /data/a.db /bak/a_$(date +%F).db # 对 0 3 * * * cp /data/a.db /bak/a_$(date +\%F).db
截断后的命令常常还能跑出个结果,只是文件名不对,所以现象很迷惑人。
3、用户字段别写错。crontab -e编辑的是5段(分 时 日 月 周 命令),而/etc/crontab和/etc/cron.d里的文件是6段,命令前面多一个用户名:
# /etc/cron.d/myjob SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 0 3 * * * root /opt/job/backup.sh
在crontab -e里写6段,cron会把用户名当命令执行,日志里出现"not found"之类的报错。反过来在/etc/cron.d里写5段,命令就被当成用户名了。
4、/etc/cron.d下的文件属主必须是root,权限不能有组写或其他人写。踩过一次,图省事chmod 666,cron直接拒绝加载,日志里写着INSECURE MODE:
chown root:root /etc/cron.d/myjob chmod 644 /etc/cron.d/myjob
环境变量才是大头
cron给任务的PATH只有/usr/bin:/bin,SHELL是/bin/sh。手动执行时用的python3.11在/usr/local/bin下、conda环境在/root/miniconda3下、node在/usr/local/node/bin下,这些路径cron一概不知道。脚本一跑就报command not found,而日志又只在邮件里,表面上就是"没执行"。
三种改法,任选一种:
crontab顶部显式声明PATH,对当前用户所有任务生效:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin MAILTO="" 0 3 * * * /opt/job/backup.sh
命令里全用绝对路径,简单粗暴也最不容易出错:
0 3 * * * /usr/local/bin/python3.11 /opt/job/sync.py >> /var/log/sync.log 2>&1
脚本内部自己补齐环境,适合依赖比较多的场景:
#!/bin/bash source /etc/profile source /root/.bashrc 2>/dev/null cd /opt/job || exit 1 /usr/local/bin/python3.11 sync.py
cron不会自动加载~/.bashrc和/etc/profile,这跟登录shell不一样,写在那些文件里的export在cron里一律拿不到。
MAILTO=""那一行建议加上。不加的话cron会把输出寄给本地用户,机器上没装MTA,邮件就堆在/var/mail或者干脆丢掉,报错信息一条都看不到。
脚本自身的问题
手动能跑、cron跑不了,还有一小部分是脚本本身。查这几个点:
有没有执行权限,chmod +x /opt/job/backup.sh。shebang写对没有,用到数组、[[ ]]就得是#!/bin/bash而不是#!/bin/sh。文件是不是从Windows传过来的CRLF行尾,这个报错很典型:
/bin/bash^M: bad interpreter: No such file or directory # 修: sed -i 's/\r$//' /opt/job/backup.sh
脚本里的相对路径也别用。cron执行时的工作目录是用户的home,不是脚本所在目录,写./config.ini读到的根本不是想要的那个文件。
时间对不上
cron按系统时区跑,容器默认是UTC,宿主机是CST,看起来就像任务晚执行了8小时。查一下:
timedatectl | grep "Time zone" date
改完时区要重启cron才生效,这是实测确认的,只改timedatectl不重启,任务还是按老时区触发:
timedatectl set-timezone Asia/Shanghai systemctl restart cron
一套能直接抄的排查顺序
碰到任务没跑,按这个来,基本几分钟内能定位:
systemctl status cron # 1 服务活着吗 crontab -l # 2 任务写进去了吗 journalctl -u cron --since "1 hour ago" # 3 触发了吗,报什么错 /bin/bash -x /opt/job/backup.sh # 4 单独跑一遍看过程 env -i /bin/bash -c /opt/job/backup.sh # 5 空环境跑,模拟cron的真实环境
第5步最有用。env -i会清掉所有环境变量,跑不通就说明脚本依赖了外部变量,表现出来的正是"手动可以、cron不行"。
数据截至2026年9月,以上在Ubuntu 22.04、24.04与RHEL 8.10、9.4、9.5,都验证过。