1. 先说清楚这个报错到底长什么样在跑docker run的时候遇到下面这段日志几乎是每个用过 Docker 的人都绕不开的一个坎docker: Error response from daemon: OCI runtime create failed: container_linux.go:235: starting container process caused permission denied: unknown.或者是container_linux.go:235: starting container process caused exec: \sh\: executable file not found in $PATH: unknown.这两种是最高频的变体但实际场景里远不止这两个。container_linux.go:235是容器运行时runc在特定版本里的一个报错锚点它本身并不是错误的根因而是“容器启动流程中断”时程序吐出来的一行上下文信息。换句话说真正的问题是它后面引号里那句描述比如permission denied、exec format error或者某个二进制文件找不到。这篇文章就专门围绕这个报错展开把常见的触发原因、排查路径、解决方案都捋一遍。做运维或者平时自己用 Docker 搭环境的朋友只要遇到过类似日志跟着下面的步骤走一遍基本能解决大部分问题。需要先说清楚一个很关键的概念container_linux.go:235里的行号是编译时的源码位置不同版本的 runc 行号会变。所以你要是拿这个报错去搜索引擎里搜会看到各种五花八门的解释有些说改权限有些说重装 Docker其实都是因为背后的触发条件不一样。先判断是哪一类错误再动手去改配置才是正确的姿势。2. 从根因层面拆解容器启动流程卡在哪个环节要真正理解这个报错得先搞清楚 Docker 启动一个容器时底层到底做了什么。当你敲下docker run请求会经过 Docker daemondaemon 把镜像解包、创建容器的文件系统然后调起一个叫做 runc 的进程。runc 负责按照 OCI 规范把容器跑起来它做的事包括准备 rootfs、设置 cgroups 资源限制、挂载 proc 和 sysfs、设置 namespaces、切换 root、最后执行用户指定的命令。整个过程由很多个小步骤组成任何一个步骤出问题runc 就会返回错误而container_linux.go:235就是这些错误在特定版本里的一个“出口”。这个报错的本质可以理解为——runc 在某个环节发现执行条件不满足但错误信息被截断了或者被压缩成了一个不够直观的短语。最常见的两种情况权限类错误。比如permission denied通常和容器内要执行的程序没有可执行权限、挂载目录权限不对、SELinux 拦截有关。可执行文件问题。比如executable file not found in $PATH说明容器入口命令在指定路径里不存在或者 shell 路径写得不对。还有一类比较隐蔽的是执行格式错误exec format error通常是因为镜像和宿主机 CPU 架构不匹配比如在 x86 机器上跑 ARM 镜像或者在老内核上跑需要新指令集的程序。把问题定性比直接盲目地改/etc/docker/daemon.json重要得多。我在实际排查中养成了一个习惯先看完整的错误日志搞清楚是哪个环节报出来的再决定怎么修。很多人一上来就重装 Docker结果搞了半天发现是镜像本身的问题白折腾。2.1 如何快速定位是哪一类错误定位的第一步是拿到更完整的错误输出。docker run默认只显示一行汇总错误但其实 daemon 的日志里往往有更详细的信息。可以用下面的方式查看sudo journalctl -u docker --since 5 minutes ago或者如果 Docker 是以服务方式跑的直接看/var/log/messages、/var/log/syslog。如果是桌面环境可以用docker info确认当前使用的 storage driver 和 runtime 配置。这些信息能帮你判断是底层环境问题还是镜像问题。第二步是针对不同的错误短语做归类。下表是常见错误短语和对应的排查方向建议存一份备用错误短语触发原因优先排查项permission denied权限不足可执行权限、挂载目录权限、SELinuxexecutable file not found入口命令不存在镜像内 shell 路径、ENTRYPOINT 写法exec format error架构不匹配镜像平台、宿主机 CPU 架构no such file or directory动态库或解释器缺失静态编译、动态链接依赖operation not permitted内核能力不足capabilities、seccomp 配置经常有人把exec format error误判成权限问题因为日志里只给了一行很容易忽略掉format这个词。如果你拿到的是这类错误优先检查镜像的Architecture字段用docker inspect就能看到docker inspect --format{{.Architecture}} 镜像名如果镜像架构是arm64而宿主机是amd64就需要加--platform linux/amd64拉取对应平台镜像或者重新构建。3. 最高频的触发场景与对应解法结合这些年踩过的坑我把这个报错按照“现场”分成几类每一类都有比较固定的解法。下面的内容可以在实际遇到问题的时候直接对照操作。3.1 权限问题数据目录没有正确授权这是出现频率最高的一种情况。典型场景是宿主机上有一个目录容器启动时需要挂载进去但这个目录的属主和权限不满足容器内进程的要求。举个例子某次用 MySQL 镜像跑数据库容器宿主机上的数据目录是 root 用户所有而 MySQL 容器内的进程用 mysql 用户运行结果docker run直接报出container_linux.go:235: starting container process caused permission denied。为什么会这样因为容器内进程在执行主命令之前runc 需要完成一系列初始化动作包括切换用户身份。如果容器镜像中指定的用户对挂载进去的目录没有读写权限启动阶段就会失败而不是等到数据库真正写数据时才报错。解决思路有两个方向。第一个是把宿主机目录的属主改成容器内用户对应的 UID/GID。比如 MySQL 镜像是用 UID 999 跑执行sudo chown -R 999:999 /data/mysql第二个方向是在docker run时显式指定用户并保证目录可写docker run -d --name mysql-test \ -v /data/mysql:/var/lib/mysql \ -u mysql \ mysql:8.0这种情况下要先确认/data/mysql对 UID 999 可写。判断用户对应关系可以这么看docker run --rm -it mysql:8.0 id mysql输出里会直接给出 UID 和 GID然后chown到对应的数值即可。这个做法是处理挂载目录权限问题最稳妥的通用方案。3.2 入口命令与执行路径问题镜像根文件系统与宿主机的差异第二种高频场景是镜像的ENTRYPOINT或者CMD写的路径有问题或者执行方式与宿主机环境不兼容。轻量基础镜像很爱出这个问题。比如基于alpine的镜像默认 shell 是/bin/sh但有的人在自建镜像时把CMD写成了/bin/bash而 alpine 里根本没有 bash。启动时就报container_linux.go:235: starting container process caused exec: \/bin/bash\: stat /bin/bash: no such file or directory处理这类问题关键要看清楚报错里引号内到底写的是什么路径。既然报错指向了执行程序的路径就要确认这个程序在镜像内是否存在、是否可执行。排查手法很直接先启动一个不进主程序的容器docker run --rm -it --entrypoint sh 镜像名如果sh也进不去那就用 debug 模式直接覆盖入口docker run --rm -it --entrypoint /bin/ls 镜像名 /把根目录列出来看看 shell 到底在哪。很多情况下是构建镜像时漏装了程序或者ENTRYPOINT用了绝对路径但镜像内目录结构被改动过。另一个容易忽略的点是CMD的写法。Dockerfile 有两种写法exec 形式和 shell 形式。CMD [/bin/sh, -c, myapp]是 exec 形式CMD myapp会被默认包一层/bin/sh -c。如果你用了 shell 形式镜像里必须要有/bin/sh否则一样会触发container_linux.go报错。3.3 宿主机的运行时环境干扰SELinux 和 AppArmor 是隐形杀手第三个高频场景不常被提到但当它出现的时候排查难度直线上升——SELinux 和 AppArmor 等安全模块。在启用了 SELinux 的 Linux 发行版上比如 CentOS、Fedora、RHEL 这一系Docker 挂载宿主目录时如果没做正确的标签处理容器进程访问这些目录就会触发permission denied。日志里看到的信息往往非常模糊翻内核日志才能看到avc: denied的字样。sudo ausearch -m avc -ts recent如果确认是 SELinux 拦截有几种处理方式。第一种临时关闭 SELinux 验证是否是它导致sudo setenforce 0跑通了再设置回 enforcing注意这只是验证手段不建议长期在生产环境关闭。第二种给挂载目录打上正确的标签sudo chcon -Rt svirt_sandbox_file_t /data/mysql第三种是直接在docker run时加上--security-opt labeldisable这个参数表示容器运行时不强制进行 SELinux 标签检查。大多数测试场景下这个参数能绕开问题但要意识到它牺牲了一部分安全隔离能力。AppArmor 的情况类似。Ubuntu 默认带 AppArmor 配置如果某次更新后 Docker 或容器内程序的 profile 出了问题也会表现为容器启动失败。用sudo dmesg | grep -i apparmor能看到相关记录临时绕过可以加--security-opt apparmorunconfined。3.4 架构不匹配ARM 镜像和 x86 宿主机之间的冲突第四类问题是架构不匹配。很多人用树莓派、开发板这类 ARM 设备或者反过来在 x86 服务器上拉取 ARM 镜像启动时就会报exec format error。这类错误不是简单的权限问题它是内核在尝试执行某个 ELF 文件时发现指令集不兼容直接拒绝加载。报错在container_linux.go:235上的表现就是starting container process caused exec format error: unknown遇到这类问题先看宿主机的架构uname -m输出如果是aarch64那就只能跑 ARM64 容器如果是x86_64就拉 amd64 镜像。跨架构跑需要额外配置模拟环境不是默认支持的。但 Docker 也提供了一个便捷参数docker run --rm -t --platform linux/amd64 ubuntu uname -m这个参数会尝试用模拟方式拉取并执行不同平台的镜像在大多数场景下能跑起来。但要注意模拟方式执行效率低而且依赖宿主内核支持相关 binfmt 模块某些老内核上不一定能正常工作。生产环境建议直接使用匹配的镜像平台不要依赖模拟。4. 一套可复用的排查命令序列与其记零散的解决方案不如掌握一套完整的排查流程。下面是我在实际工作中多次使用的命令序列从定位到修复按顺序跑就能覆盖绝大多数docker run报错场景。4.1 第一步拿到完整错误信息docker run默认的错误提示过于简短先通过以下命令拿到更完整的输出# 使用 --rm 避免留下脏容器 docker run --rm -it 镜像名 # 如果嫌信息不够加上 debug 环境变量 DOCKER_API_VERSION1.40 docker run --rm -it 镜像名 # 查看 daemon 日志 sudo journalctl -u docker --no-pager | tail -100为什么要看 daemon 日志因为docker run客户端很多时候只显示一行汇总信息而 daemon 日志里会记录更底层的运行时错误甚至包含 runc 的完整报错路径能帮助我们定位是哪个挂载点或命令出了问题。4.2 第二步验证容器内文件系统拿到报错后立刻确认容器内的可执行文件和动态库是否完好。不要急着改宿主机配置先用调试模式进入容器# 覆盖入口直接看文件系统 docker run --rm -it --entrypoint /bin/ls 镜像名 /bin/ # 查看动态库依赖 docker run --rm -it --entrypoint /usr/bin/ldd 镜像名 /app/程序路径 # 确认 shell 是否存在 docker run --rm -it --entrypoint /bin/sh 镜像名 -c which sh如果/bin/ls都进不去那说明镜像本身有问题或者 runc 在准备阶段就失败了。此时优先怀疑的是镜像损坏、rootfs 不完整、或者宿主机存储驱动与镜像不兼容。可以试试重新拉取镜像docker pull 镜像名或者切换 storage driver。overlay2是目前最主流的选择但如果你的文件系统不支持可以尝试vfs{ storage-driver: vfs }修改 daemon.json 后重启 Dockersudo systemctl restart docker注意 vfs 性能较差占用空间大只是用来验证问题原因的临时方案。4.3 第三步逐项排查权限与安全策略文件系统没问题的话接着检查权限与安全策略# 查看挂载目录权限 ls -lZn /data/mysql # 查看容器进程要用的 UID docker run --rm -it --entrypoint id 镜像名 # 检查 SELinux 审计日志 sudo ausearch -m avc -ts recent这里有个实用技巧如果ls -lZ输出里挂了 SELinux 标签比如以system_u:object_r:default_t结尾很可能就是标签不匹配。用chcon改成svirt_sandbox_file_t一般能解决问题。4.4 第四步检查资源限制与内核参数有时候报错不是权限也不是架构而是容器需要的资源不足。比如进程尝试分配共享内存但/dev/shm太小或者需要更多的内存限制。这类错误也会在容器启动阶段被 runc 汇总为同样的报错样式。检查当前的限制docker info | grep -A5 Default Runtime # 检查系统最大进程数 sysctl kernel.pid_max # 检查可用内存 free -h如果是/dev/shm不够可以在docker run时加大docker run --shm-size1g 镜像名如果是进程数达到上限可以通过调整内核参数缓解但这属于宿主环境层面的问题需要综合评估后再改。5. 从镜像构建角度避免踩坑很多docker run报错问题其实早就在构建镜像的时候就埋下了。与其在运行阶段反复排查不如在写 Dockerfile 时就把几个关键点做对。5.1 尽量使用 exec 形式写入口Dockerfile 里的CMD和ENTRYPOINT有两种写法。推荐用 exec 形式CMD [myapp, --config, /etc/myapp/config.yaml]因为 shell 形式会多一层/bin/sh -c解释如果镜像里没有/bin/sh容器就无法启动。用 exec 形式还能保证进程收到的信号是直接的应用可以优雅退出而不是被 shell 拦截后变成孤儿进程。有一个容易踩的坑是如果你用了ENTRYPOINT加上外部参数的形式比如ENTRYPOINT [/bin/sh, -c] CMD [myapp]那容器启动时运行的就是/bin/sh -c myapp。这种方式可以拼接参数但对信号处理不友好而且如果/bin/sh不存在报错是必然的。5.2 明确设置容器用户构建镜像时USER指令尽量显式指定。不写USER默认以 root 运行虽然省事但会带来两类问题一是启动后进程权限过大有安全风险二是挂载目录的属主和容器内用户不一致时反而更容易触发权限类启动失败。建议这样写RUN useradd -u 1001 -m appuser USER 1001运行时如果需要挂载数据目录直接把宿主机目录 chown 给 1001 即可。这个思路适合大多数 Web 应用和数据库类镜像。5.3 静态编译 vs 动态编译如果你的应用是编译型语言写的比如 Go、Rust或者 C/C需要特别注意动态库依赖。容器基础镜像里未必有你本机编译时链接的那些 .so 文件。建议使用静态编译。以 Go 为例编译时加CGO_ENABLED0 go build -a -ldflags -s -w -o myapp .这样生成的二进制不依赖任何动态库放进精简镜像里也能直接运行。如果是 C 程序可以用-static参数静态链接。判断一个程序是否静态链接用file myapp如果输出里出现statically linked就放心用如果出现dynamically linked则需要确认镜像里有对应的库。这一条看起来不起眼但能解决大量exec format error和隐形的no such file or directory问题。很多人遇到容器启动失败第一反应是环境问题实际上二进制文件在基础镜像里缺了依赖。6. 经典问题速查表与实际案例实录把上面讲的这些整理成一张速查表方便在实际运维时快速对照。这张表也适合打印出来贴在工位旁边排查的时候能省不少时间。错误特征根本原因最快的修复方案报错引号内含 permission denied挂载目录有关联权限不足chown 挂载点为容器 UID或目录加宽松权限报错引号内含 executable file not found入口命令不存在换路径或镜像内安装对应程序报错引号内含 exec format error架构不匹配加--platform拉取对应架构镜像报错引号内含 no such file or directory动态库缺失或解释器不存在静态编译应用或镜像内补齐依赖报错引号内含 operation not permitted内核能力受限调整 capabilities、seccomp或加--privileged验证排除上述所有原因仍报错daemon 或 runc 状态异常重启 Docker或升级 runc 版本再说一个实际的案例。某次在一个服务器上跑一个监控采集容器宿主系统是某 Linux 发行版容器镜像基于老版本的基础镜像构建。docker run起来就报container_linux.go:235: starting container process caused process_linux.go:541: setting up seccomp: read /proc/self/status: permission denied。这种情况非常奇怪因为 seccomp 是内核安全机制正常情况下 runc 读取自身状态不应该被拒绝。排查后发现是那个发行版发行时带的旧版 runc 与新的内核之间存在兼容性问题。升级 runc 之后问题消失。这类问题用常规的权限排查方法根本定位不了因为没有文件权限错误也不是网络问题。经验就是在确认前面几类原因都排除后尝试升级 Docker 或 runc 本身往往能解决一批“说不清原因”的启动失败。再来一个问题有位同事跑一个 Java 应用容器启动时报container_linux.go:235: starting container process caused exec: \/usr/bin/java\: stat /usr/bin/java: no such file or directoryJava 应用的镜像用的是eclipse-temurin这个镜像的 Java 路径在不同的 tag 下其实不一样。有的 tag 里java在/usr/bin/java有的在/opt/java/bin/java。把CMD改成通过which java动态定位或者直接在 Dockerfile 里设置ENV PATH问题就解决了。如果你遇到 Java 容器报路径不存在先别急着怀疑镜像损坏docker run --rm -it 镜像 which java看下输出位置。7. 预防胜于修复日常使用中的几个好习惯说实话这类报错很多都是因为使用习惯不够规范。养成下面几个习惯能大幅减少遇到container_linux.go类报错的概率。第一构建镜像时在本地先跑一遍再推到仓库。很多人只在 CI 上构建推完就直接部署等到部署环境发现起不来才去查问题。其实本地docker run一下是最快的验证方式哪怕只是启动后立刻退出也能提前拦截大部分入口命令、权限、依赖问题。第二给镜像打 tag 时不要只用latest。某个基础镜像升级后新 tag 可能改了默认用户、路径或者运行环境直接导致你的容器启动失败。固定到具体的版本 tag配合明确的依赖清单至少能在出问题时快速回滚而不是跟一个不稳定的latest较劲。第三宿主机内核和 Docker 版本保持更新到稳定分支。这个建议可能听起来像废话但很多奇怪的容器启动失败都是因为 Docker 太老、内核太新或者反过来——两者之间的兼容性出现了缺口。尤其是 runc 相关的报错升级版本往往能带来意想不到的修复。比如之前提到的那次 seccomp 兼容问题其实就是升级解决掉的。第四理解权限模型而不是简单地用--privileged绕开一切。很多朋友遇到权限问题第一反应就是加--privileged或者--security-opt seccompunconfined这确实能解决 90% 的权限类启动失败但副作用极大容器几乎等同于拥有了宿主机的全部权限。如果确认是业务需求用白名单式的最小授权方案比如docker run --cap-add SYS_ADMIN --security-opt apparmorunconfined 镜像名只添加需要的 cap其他保持默认限制。这类细节在单机开发环境里无所谓但在生产环境里是底线问题。我见过因为这个问题被黑的生产事故代价远比省几分钟排查时间大得多。第五用好docker inspect和docker logs不要只盯着docker run那几行错误。docker logs拿的是容器内程序的标准输出和标准错误很多情况下程序已经启动了一部分只是初始化失败退出真正的报错信息能在日志里看到。比如某个程序需要连数据库但没配网络docker run会报容器退出而docker logs里能看到connection refused。启动类报错和运行时报错不要在同一个排查思路上死磕。8. 个人实操中的最后几点心得做这一行时间久了你会形成一种直觉看到错误日志的瞬间先判断是“环境问题”还是“代码问题”。container_linux.go:235这个报错绝大多数时候是环境问题但也很容易让人误以为是代码或者镜像的问题。我的经验是——先冷静读日志把引号里的错误短语抠出来。不要看那一长串栈信息那只是 runc 的出口通道。看引号内容才是排查的真正起点。还有一个容易忽略的维度是网络。某些内网环境拉镜像时走了代理或者镜像仓库配置了不安全的 HTTP 源导致镜像拉取不完整最终表现也是容器启动各种奇怪报错。如果排查完所有本地问题仍然没解决建议检查一下镜像层是否完整docker history 镜像名看看有没有残留的missing层以及docker pull是否有中断痕迹。镜像不完整的情况下任何奇怪的问题都可能出现。最后想说的是Docker 这个工具无论如何抽象最终还是要跟 Linux 内核打交道。理解 namespaces、cgroups、capabilities、seccomp 这些底层概念解决这类报错就会从容许多。本文提到的所有命令建议都在自己的实际环境里跑一遍验证而不是背诵。遇到报错不可怕可怕的是把“跑通”当目的、把“理解”当成本。希望这篇文章能帮你在下一次遇到container_linux.go:235的时候少走弯路一击即中。