news 2026/10/21 4:28:18

Docker OCI runtime报错排查:从container_linux.go到容器启动失败根因

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker OCI runtime报错排查:从container_linux.go到容器启动失败根因

前阵子帮一个团队排查内部工具容器化的问题,控制台只甩给我一行:

docker: Error response from daemon: OCI runtime create failed: container_linux.go:235: starting container process caused "exec: \"app\": executable file not found in $PATH": unknown

第一眼确实容易懵:镜像明明构建成功了,容器怎么起不来?“OCI runtime error”这个前缀又长又拗口,看起来像是运行时层面的严重故障,实际上绝大多数情况只是一条“镜像里某个启动文件不对”的消息被包装成了这种格式。为了讲清楚这类报错,我拿这个具体问题作为例子,把从报错信息到根因定位的完整排查过程拆开来讲,顺带把我处理过的几个典型场景一并复盘。如果你也撞上过container_linux.go:235或者它换行后的某个container_linux.go行号,这篇文章应该能帮你省下不少时间。

1. 先看懂这个报错:runc 在哪个环节卡住了

1.1 一条 docker run 命令背后发生了什么

Docker 本身并不直接创建容器进程,它只是“前台的门面”。一条docker run命令发出去之后,请求会经过dockerd,转给containerd,最后由runc这类 OCI runtime 去真正创建容器。container_linux.go是 runc 源码里的一个文件,专门负责 Linux 容器进程的初始化、启动和退出处理。不同版本的 runc 行号会变,所以网上有人整理过container_linux.go:235的对应关系,但同一份代码在不同发行版里编译后的行号并不完全一致,死磕这个行号意义不大。

这个报错真正想告诉你的是:容器进程在“创建/启动”阶段失败了,而具体失败原因就在starting container process caused后面那一小段里。如果只盯着container_linux.go:235,很容易走偏,甚至去重装 Docker、重置内核参数,最后才发现其实是 Dockerfile 里一个手误。

1.2 报错里真正有用的信息藏在冒号之后

以我上面那条报错为例,把它拆开看:

  • OCI runtime create failed:runc 创建容器失败,是个兜底描述。
  • container_linux.go:235:源代码位置,说明失败发生在 runc 的 Linux 容器进程启动逻辑里。
  • starting container process caused:后面跟着的是底层系统调用返回的具体原因。
  • exec: "app": executable file not found in $PATH:runc 在容器里执行app这个命令时找不到可执行文件。

前面三部分都是“壳”,最后这一句才是真正需要分析的。常见的后半句大致有几种:

后半段出现的关键词真实含义优先排查方向
executable file not found in $PATH命令不存在于 PATH 中Entrypoint/CMD 拼写、基础镜像差异
no such file or directory启动文件或动态库缺失文件是否 COPY 进去、挂载是否覆盖
permission denied没有执行权限或被安全策略拦截文件权限、selinux/AppArmor、挂载权限
exec format error架构不匹配或可执行格式不对镜像平台、二进制编译方式

拿到这层信息之后,才算真正开始排错。

2. 排查链路:把“通用报错”变成“具体问题”

2.1 用 exit code 和容器状态缩小范围

docker run一次性失败的时候,可以先看容器退出码。运行容器后立刻执行:

docker ps -a

假如容器停在Exited (127),那问题基本可以锁定在“启动命令不存在”。因为 127 是 Linux 里“command not found”的经典退出码;如果退出码是 126,一般是“命令存在但不可执行”;如果是 2 或 1,那可能是脚本自身逻辑或者参数有问题。接下来用 inspect 看更完整的运行信息:

docker inspect --format '{{.State.ExitCode}} {{.State.Error}}' <container_id_or_name>

State.Error里往往会有比终端控制台更完整的报错文本。另一个很有用的字段是Path和Args,它们记录了 runc 最终尝试执行的程序路径和参数,能够直接揭示容器启动时到底跑了什么。举个我常做的操作:

docker inspect --format 'Path={{.Path}} Args={{.Args}}' <container_id>

如果显示Path=/bin/sh但Args=[/app/start.sh],就说明 Entrypoint 是 shell,实际执行的是脚本。这时候如果脚本有问题,通常会留下更重要线索。

2.2 用 docker inspect 验证 Entrypoint 和 Cmd

镜像本身的启动配置可以通过docker image inspect快速确认。我一般直接看两块:

docker image inspect --format '{{json .Config.Entrypoint}}' <image> docker image inspect --format '{{json .Config.Cmd}}' <image>

Entrypoint是容器启动时必须执行的命令,Cmd要么作为 Entrypoint 的默认参数,要么在 Entrypoint 为空时充当默认命令。很多报错就是因为这两个字段里写了一个不存在路径。举个例子,如果有人把ENTRYPOINT ["python"]写成了ENTRYPOINT ["Python"],大小写不一致,Linux 文件系统会直接找不到文件;Launch 时报的正是“no such file or directory”,但一开始你甚至会怀疑 Python 没装。

2.3 用 known-good 镜像判断运行时是否健康

如果排除了镜像里的路径和命令配置,仍然报同样的错误,那就需要判断是 Docker 运行时环境问题还是镜像自身问题。我会先跑一个最小的官方镜像,比如:

docker run --rm hello-world

这个镜像启动后只打印一句提示就退出。如果它都能正常打印输出,说明 dockerd、containerd、runc 这一层基本没问题,问题大概率出在目标镜像本身;反过来,如果连hello-world都在container_linux.go报错,那就要检查 Docker 版本、内核版本、cgroup 驱动、存储驱动,甚至考虑重启 Docker 引擎。注意,hello-world正常但目标镜像失败,并不能完全排除运行时问题,因为不同镜像可能触发不同的系统调用,但至少能把排查范围砍掉一大半。

3. 逐个击破:入口文件、动态链接库、挂载目录

3.1 Entrypoint / CMD 指向的文件不存在

这是container_linux.go:235这条报错里出现频率最高的一种根因。常见触发方式包括:

  • Dockerfile 写了ENTRYPOINT ["/opt/app/start.sh"],但最后构建阶段根本没有 COPY 这个文件,或者 COPY 路径不对。
  • 基础镜像换过。比如原来基于 Ubuntu,改成 Alpine 后,Dockerfile 里还写着bash,但 Alpine 默认没有 bash。
  • 镜像里的WORKDIR被改过,启动脚本却用了相对路径。

定位时,我会先用一个覆盖 Entrypoint 的命令进入容器,检查目标文件是否存在:

docker run --rm --entrypoint /bin/sh <image> -c "ls -l /opt/app/start.sh"

如果报no such file or directory,马上就能确认文件缺失。还有一个容易被忽略的细节:镜像里 Entrypoint 指定的路径和实际路径一致,但容器里只能看到“文件存在”而不可执行,比如权限位是 644。这时启动报错通常是permission denied,而ls -l的输出里x位为空。构建阶段忘记chmod +x是常见操作,修起来也简单,在 Dockerfile 里加一行:

COPY start.sh /opt/app/start.sh RUN chmod +x /opt/app/start.sh

3.2 文件在但“跑不起来”:动态链接库与换行符

另一种很坑的情况是文件确实存在、权限也没问题,但 runc 执行时仍然报错。我遇到过两类:

第一类是动态链接库不完整。如果你把一个在一台 Ubuntu 上编译的 Go 或 C 程序直接扔进 Alpine 镜像跑,程序动态链接的 libc 路径对不上,就会报exec format error或者no such file or directory。检查方法是在容器里手动执行一次ldd:

docker run --rm --entrypoint /bin/sh <image> -c "ldd /opt/app/start"

如果看到not found字样的动态库,说明运行时库里缺依赖。更好的解决办法是构建时使用纯静态编译:CGO_ENABLED=0,这样程序不再依赖目标镜像的 libc,我后面会再展开。

第二类是脚本文件本身存在,但第一行带着 Windows 的换行符。很多同事在 Windows 下编辑 shell 脚本后直接提交到仓库,构建进镜像之后,脚本第一行变成了#!/bin/sh\r。内核在解析 shebang 时会把/bin/sh\r当作解释器路径,而 Linux 文件系统里并没有这个名字,于是报错居然是“no such file or directory”,明明文件就在那儿。检查方法很简单:

docker run --rm --entrypoint /bin/sh <image> -c "head -1 /opt/app/start.sh | od -c | head -2"

输出里如果看到\r,就把脚本换行统一成 LF,或者在构建阶段加dos2unix。这个坑我在真实项目里遇过不止一次。

3.3 挂载目录把启动路径“顶掉了”

镜像本来启动正常,但加了-v之后突然报同样的 OCI runtime error,这是第三个高频场景。比如我在排查某个服务时,习惯把自己宿主机的配置目录挂载到/app/config,但那个服务的 Entrypoint 恰好就在/app/config/start.sh里。挂载之后,宿主机目录把镜像里原本存在的/app/config整个覆盖掉,runc 去执行/app/config/start.sh,发现目标路径已经被宿主机的空目录替换,自然就“找不到文件”了。

遇到这类情况,先看docker inspect里的Mounts字段:

docker inspect --format '{{json .Mounts}}' <container_id>

确认挂载点和 Entrypoint 是否存在重叠。如果确实重叠,要么把挂载点迁到/data之类的独立目录,要么把启动脚本放到不会被挂载覆盖的路径。我在真实项目里遇到过团队为了图省事,把整个/opt挂载到宿主机,结果每次发布都要手工确认/opt/app是否还在,迟早出问题。

4. 三个真实案例复盘

4.1 Alpine 镜像里用 bash,报 executable file not found

有一次同事构建镜像,Dockerfile 开头是:

FROM alpine:3.18 RUN apk add --no-cache curl ENTRYPOINT ["bash", "/start.sh"]

构建没问题,但docker run立刻报:

oci runtime error: container_linux.go:235: starting container process caused "exec: \"bash\": executable file not found in $PATH": unknown

我第一反应是bash没装。Alpine 默认的 shell 是/bin/sh,它不内置 bash。用命令验证了一下:

docker run --rm --entrypoint /bin/sh <image> -c "which bash || echo not-found"

输出确实是not-found。修复方案有两种,一是在 Dockerfile 里加apk add --no-cache bash,二是直接把 Entrypoint 改成/bin/sh。当时脚本本身只是简单启动服务,不依赖 bash 特性,所以我选择了后者,镜像体积还能小一点。这个案例的教训是:换基础镜像时,所有启动命令和动态依赖都要重新过一遍,不能只看应用包是否拷贝进去了。

4.2 Shell 脚本第一行被 Windows 换行符污染

另一个案例里,脚本存在、权限正确,但报错是:

starting container process caused "exec: \"/app/entrypoint.sh\": no such file or directory"

我反复确认/app/entrypoint.sh就在镜像里,甚至用ls -l都能看到文件。后来用od查看第一行字节,发现末尾跟着0d 0a。由于 Dockerfile 是从 Windows 仓库拉下来的entrypoint.sh,它在 Windows 里编辑后保存为 CRLF。内核执行脚本时,把/bin/sh\r当成解释器去查找,自然找不到。修复是在构建阶段统一转换:

FROM debian:bookworm-slim COPY entrypoint.sh /app/entrypoint.sh RUN sed -i 's/\r$//' /app/entrypoint.sh && chmod +x /app/entrypoint.sh

从那以后,我再看到“文件明明存在却 no such file or directory”这类报错,就会先怀疑换行符,而不是第一时间怀疑 Docker 或内核。

4.3 挂载路径覆盖启动脚本导致容器秒退

还有一次,服务在本地直接docker run是好的,放到编排系统里就起不来。本地的执行命令是:

docker run -v /data/conf:/conf myapp

后来排查发现,镜像里 Entrypoint 写的正是/conf/start.sh,而宿主机/data/conf是个空目录。挂载以后,容器里的/conf/start.sh被宿主机空目录替代,启动进程找不到入口。这类问题很有迷惑性,因为镜像本身没问题,Dockerfile 也没问题,错的只是运行时的“入参”。我还是通过docker inspect看到Mounts和Path才把问题串起来。之后的约定是:启动脚本所在目录必须与用户数据挂载目录分开,如果确实无法分开,镜像里就得多做一层“文件不存在时用默认入口”的容错逻辑。

5. 后续改进:把这类报错挡在构建阶段

5.1 构建镜像时顺手做三件小事

经历了上面这些案例后,我给自己定了几条构建阶段的检查项,能在很大程度上减少“容器起不来”的问题:

  • 基础镜像尽量固定 tag,并在 Dockerfile 里写明预期的 shell 和运行环境。不要默认 Alpine 一定适合所有场景,也不要默认 Ubuntu 一定包含所有库。
  • 所有 COPY 进镜像的脚本都显式设置可执行权限,或者在RUN阶段统一chmod +x。
  • 在镜像构建最后加一条“启动命令自检”的 RUN 命令,比如:
RUN test -x /opt/app/start.sh && echo "entrypoint ok"

这样如果启动文件缺失或权限不对,构建阶段就会报错,根本不会拖到运行时。虽然增加了一点构建时间,但比线上半夜收到告警要划算得多。

5.2 多阶段构建里容易忽略的动态库问题

多阶段构建本身没问题,容易踩坑的是“构建阶段编译出来的二进制,运行阶段镜像却跑不起来”。尤其是使用 CGO 的 Go 程序,或者直接编译 C/C++ 程序时,默认会动态链接 glibc。构建阶段基于 Ubuntu,运行阶段换成 Alpine,非常容易出现缺少ld-linux-x86-64.so.2之类的问题。

我的建议是,如果应用没有强制使用 CGO,尽量用静态编译:CGO_ENABLED=0。如果必须使用动态链接,那就让运行阶段的基础镜像和构建阶段保持同一发行版系,至少 libc 实现要一致。可以选 Debian slim 或 Distroless,而不是强行用 Alpine 去兼容所有二进制。编译完成后在宿主机上先跑一步:

file ./app ldd ./app

看到statically linked基本就能安心跨镜像使用;看到一堆动态库路径,就得留心运行镜像里是否有这些路径。

5.3 把“启动自检”留在容器前面

即使构建阶段做了自检,容器运行环境的挂载、环境变量、用户权限仍然可能在最后一刻改变结果。我现在会在启动脚本开头加一小段探测逻辑,比如检查关键文件是否存在、当前用户是否有权限:

if [ ! -x /opt/app/start.sh ]; then echo "entrypoint missing" >&2 exit 127 fi exec /opt/app/run

脚本里exec命令尽量放在最后,让容器主进程直接接管,避免 shell 进程成为多余的父进程。这样就算最终仍然失败,docker logs里至少能看到一条明确的错误,而不是只有container_linux.go:235这种让人一头雾水的行号。

如果你也被类似的 OCI runtime 报错卡住,先记住一点:别困在container_linux.go这个文件名里。把冒号后面的starting container process caused真正读懂,再对照docker inspect里的 Path、Args、Mounts 和退出码,九成问题都能在两三步之内定位出来。这是我处理这类报错最核心的心法,也是这篇记录最想留给你的东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 4:28:00

MCP协议实战:重塑AI编程工作流,打通上下文断层

1. 从"能对话"到"能干活"&#xff1a;我为什么重写AI编程工作流前几个月我一直在折腾AI编程实践&#xff0c;主要围绕MCP&#xff08;Model Context Protocol&#xff0c;模型上下文协议&#xff09;展开。一开始我的用法非常简单粗暴&#xff1a;让AI聊天…

作者头像 李华
网站建设 2026/10/21 4:27:58

ArcGIS Pro自动选投影带:避开坐标错位的实用指南

作为一个常年泡在测绘和GIS项目里的人&#xff0c;我太清楚“选错坐标投影带”是什么滋味了。去年做一个外业数据整理&#xff0c;客户给了一堆WGS84的原始采集点&#xff0c;我为了图省事&#xff0c;随手选了个投影坐标系就开始叠加底图&#xff0c;结果明明是同一个地块的边…

作者头像 李华
网站建设 2026/10/21 4:27:37

视频孪生如何赋能低空经济:从技术架构到落地实践

这两年低空经济突然成了热门赛道&#xff0c;无人机物流、低空巡检、城市级低空治理的项目一个接一个冒出来。但很多项目推进到一半&#xff0c;最常遇到的不是无人机不好买&#xff0c;也不是空域手续难跑&#xff0c;而是&#xff1a;天上飞的数据和地下管业务的人&#xff0…

作者头像 李华
网站建设 2026/10/21 4:27:05

VxWorks下GPIO模拟MDC/MDIO驱动:PHY寄存器调试的保底方案

简介&#xff1a;面向VxWorks嵌入式开发者的GPIO模拟MDC/MDIO驱动源码包&#xff0c;为缺少专用SMI硬件接口却需配置以太网PHY芯片的场景提供纯软件实现方案。压缩包仅含1个C源码文件&#xff0c;体积约2KB&#xff0c;代码集中处理GPIO方向切换、MDC管理时钟生成、MDIO双向数据…

作者头像 李华
网站建设 2026/10/21 4:26:27

AI辅助教育科研问卷设计:从理论框架到题项生成全流程实战

问卷这东西&#xff0c;做过的都知道&#xff0c;看着简单&#xff0c;真正动手从无到有设计一份高质量的教育科研问卷&#xff0c;往往比写一篇论文的正文还折磨人。最常见的死循环是这样&#xff1a;选题方向还没完全想清楚&#xff0c;就先被"抽哪些维度""每…

作者头像 李华
网站建设 2026/10/21 4:26:25

AI重构教育科研问卷设计:从维度拆解到信效度预估的完整方案

论文问卷写不明白&#xff0c;一改就是三周&#xff0c;这是很多搞教育科研的人都有过的体验。尤其是新手阶段&#xff0c;大家普遍以为问卷设计就是“问几个问题”&#xff0c;真上手才发现根本不是那么回事——维度怎么划、题项怎么措辞、信效度怎么保证&#xff0c;样样都能…

作者头像 李华