Docker使用--cache-from构建时CMD异常转为sh -c格式的问题排查
CMD配置异常的核心触发原因
- 跨镜像缓存模式下的元数据层错配。Docker的构建缓存机制在开启
--cache-from跨镜像匹配模式时,仅对实际产生文件变更的层做文件系统哈希校验,对#(nop)标记的纯元数据层(CMD、ENTRYPOINT、ENV这类不修改文件系统的指令),仅比对指令文本和构建参数差异,不会校验父层的哈希是否一致。当你使用--cache-from拉取的旧缓存镜像是在基础层修改前构建的,哪怕当前基础层已经完全变更,只要CMD层的指令文本和旧镜像一致,缓存逻辑就会直接复用旧镜像中存储的CMD元数据。这也是为什么你通过docker history看到的CMD层指令是正确的#(nop)格式,但docker inspect读取到的实际运行时配置是异常内容——history展示的是当前Dockerfile的指令文本,而镜像实际Config字段存储的是缓存拉取的旧值,两者出现了不一致。 - Dockerfile指令格式的解析歧义。Docker支持两种CMD写法:exec数组格式和shell字符串格式。如果某次临时调试时曾将CMD改成shell格式,回滚时未完全修正数组格式的转义、分隔符配置,构建工具会自动将其转成
/bin/sh -c开头的shell存储格式,这层错误配置一旦被打入缓存镜像,后续所有拉取该缓存的构建都会复现异常格式。 - BuildKit legacy缓存模式的已知兼容bug。在18.09-20.10版本区间的Docker Engine中,BuildKit在legacy缓存模式下存在逻辑缺陷:会跳过对纯元数据层的格式校验,直接从旧镜像中拉取CMD、ENTRYPOINT的存储内容,哪怕当前Dockerfile的指令完全正确。
关于
sh -c格式CMD的兼容性说明 /bin/sh -c开头的CMD是Docker原生支持的合法存储格式,不属于异常。
Docker的命令拼接规则为:当ENTRYPOINT使用exec数组格式定义时,最终执行的命令为「ENTRYPOINT 数组 + CMD 数组」,不会自动插入shell层。如果CMD本身使用shell字符串格式定义,Docker会自动将其转换为["/bin/sh", "-c", "实际执行命令"]的数组格式存储,这是正常的语法转换逻辑。
你的自定义entrypoint.sh无法正常解析的核心原因并非sh -c格式本身,而是缓存错配导致传入的参数内容完全错误:
- 正常合法的shell格式CMD,传给entrypoint.sh的参数为
/bin/sh -c "bundle exec rackup --host 0.0.0.0 -p 3000",脚本内的exec "$@"可以正常接管启动流程,不会出现执行错误。 - 缓存异常场景下,传给脚本的参数被错误替换成
/bin/sh -c "#(nop) " 'CMD ["bundle" "exec" "rackup"...]',相当于直接把Dockerfile中CMD指令的文本内容当成shell命令字符串执行,自然无法完成业务启动。
常见修复操作
- 直接检查
--cache-from指向的缓存镜像元数据,确认异常的CMD内容是否来自旧缓存层,优先清理该缓存镜像即可消除异常。 - 若需保留缓存加速能力,可在CMD指令前插入动态构建参数(如
ARG CACHE_REFRESH=random_string),强制让构建流程跳过CMD层的旧缓存,生成正确的元数据格式。 - 将BuildKit的缓存模式调整为标准的同步模式,关闭legacy兼容开关,修复元数据层的哈希校验逻辑。
内容的提问来源于stack exchange,提问作者Dennis van de Hoef - Xiotin
相关产品推荐
相关产品推荐

