Alpine 容器里 crond 报 can’t execute ‘/bin/bash’:一次五阶段的排查
在 Alpine Linux 容器中配置 cron 定时任务时,遇到一个让人困惑的问题:crond 持续报错 can't execute '/bin/bash' for user root,但手动执行命令完全正常。这篇文章记录了从错误现象到根因定位、再到最终解决的完整排查过程。
问题现象
在 Docker 容器中,crontab 配置如下:
*/5 * * * * su-exec www-data /usr/local/bin/php /var/www/html/app/cron.php
容器日志持续输出:
app-php | crond: can't execute '/bin/bash' for user root
但进入容器手动执行:
su-exec www-data /usr/local/bin/php /var/www/html/app/cron.php
一切正常,PHP 脚本顺利执行。为什么手动可以,crond 里就不行?
排查过程
第一阶段:以为是 su 的问题
最初 crontab 里用的是 su -s /bin/sh www-data -c '...'。Alpine 使用 busybox 的 su,在 crond 非交互、无 tty 环境下会尝试启动 login shell,而编译时默认路径是 /bin/bash,Alpine 没有 bash 就报错。
解决思路:换用 Alpine 推荐的 su-exec 工具,它直接 setuid() + execve(),不经过 login shell。
apk add --no-cache su-exec
但换成 su-exec 后,报错依旧。
第二阶段:crontab 引号问题
检查 crontab 文件内容时发现命令被单引号包裹,导致 su-exec 把整个字符串当成可执行文件路径。去掉引号后参数正确分离,但报错仍然存在。
第三阶段:crond 没重载
busybox crond 启动后一次性解析 crontab,修改文件不会自动重载。需要杀掉进程重新启动:
pkill -9 crond
crond -f -l 8 -L /dev/stdout &
第四阶段:两个 crontab 文件
busybox crond 会同时读取两个位置:
/var/spool/cron/crontabs/root/etc/crontabs/root
之前只改了其中一个,另一个里还有旧命令在跑。统一后报错仍然存在。
第五阶段:找到根因
最终发现容器的 /etc/passwd 是从宿主机挂载进来的,其中 root 的条目是:
root:x:0:0:root:/root:/bin/bash
Alpine 容器里没有 /bin/bash。crond 启动时会从 /etc/passwd 读取 root 的 login shell,或者继承 SHELL=/bin/bash 环境变量,每次执行任务都尝试 exec /bin/bash,找不到就报 ENOENT。
验证:
cat /proc/$(pidof crond)/environ | tr '\0' '\n' | grep SHELL
# 输出:SHELL=/bin/bash
解决方案
方案一:设置 SHELL 环境变量(最快)
ENV SHELL=/bin/sh
或者在 crontab 文件顶部加:
SHELL=/bin/sh
方案二:不挂载主机 /etc/passwd(推荐)
RUN deluser www-data 2>/dev/null; true
RUN sed -i '/^www-data:/d' /etc/passwd /etc/group 2>/dev/null; true
RUN adduser -D -u 33 -s /bin/sh www-data
方案三:Entrypoint 里修复
#!/bin/sh
sed -i 's|/bin/bash|/bin/sh|g' /etc/passwd
export SHELL=/bin/sh
exec "$@"
额外踩坑:Alpine 删除用户和组
busybox 的 delgroup 不允许删除还有成员引用的组。解决方式:
deluser先于delgroup执行- 或者
sed -i '/^group:/d' /etc/group强制清理 - 最简洁做法:
adduser -D -u 33 -s /bin/sh www-data一步创建用户+同名组
总结
核心教训:Alpine 容器不要挂载主机的 /etc/passwd,在 Dockerfile 里管理用户;crond 的 SHELL 环境变量要显式设为 /bin/sh。
RUN apk add --no-cache su-exec
RUN deluser www-data 2>/dev/null; true
RUN sed -i '/^www-data:/d' /etc/passwd /etc/group 2>/dev/null; true
RUN adduser -D -u 33 -s /bin/sh www-data
ENV SHELL=/bin/sh
COPY crontab /etc/crontabs/root
RUN chmod 0644 /etc/crontabs/root
CMD ["sh", "-c", "crond -f -l 8 -L /dev/stdout & php-fpm -F"]