个人主页爱和冰阔乐专栏传送门《数据结构与算法》 、C学习方向C方向学习爱好者⭐人生格言得知坦然 失之淡然博主简介文章目录前言一、先别急着看日志先看容器状态1.1 “启动失败”其实至少有四种状态1.2 退出码能告诉我们多少二、建立一条固定的排障证据链三、日志先抓住第一次失败3.1 最常用的 docker logs3.2 为什么有时 docker logs 是空的3.3 入口脚本为什么经常让错误变得模糊四、端口问题区分容器端口和宿主机端口4.1 -p 18080:8080 到底是什么意思4.2 宿主机端口已经被占用4.3 端口映射正确为什么还是访问不到4.4 不要随意把数据库端口发布到所有网卡五、挂载与权限容器里是哪个用户5.1 先看进程身份5.2 不要用 chmod 777 当最终方案5.3 文件存在容器为什么说找不到5.4 SELinux 场景六、环境变量与启动命令6.1 容器中到底拿到了哪些变量6.2 Compose 变量替换和容器环境不是一回事七、网络问题先确认“谁访问谁”7.1 宿主机访问容器7.2 一个容器访问另一个容器7.3 depends_on 不等于依赖服务已经就绪八、健康检查失败但应用明明能访问8.1 HEALTHCHECK 在哪个环境执行8.2 健康检查不要检查太多东西8.3 启动宽限时间九、最小化复现比反复改配置有效9.1 从最小命令开始9.2 覆盖入口命令进入镜像9.3 比较镜像默认配置和容器实际配置总结参考资料前言Docker 容器启动失败时很多人的第一反应是dockerrestart 容器名不行就删容器、重新构建再不行就清缓存、重启 Docker。偶尔确实能恢复但问题也被一起藏起来了。下一次换一台机器、换一个端口、换一个挂载目录同样的错误还会重新出现。容器排障最麻烦的地方不是命令太多而是故障可能来自不同层镜像本身没有问题但启动命令写错了主进程能运行但没有权限读取挂载目录容器显示 Running但应用只监听127.0.0.1端口映射写对了但宿主机端口已经被占用服务已经启动但健康检查依赖了镜像里不存在的curl应用可以访问 IP却不能解析 Compose 服务名日志只显示最后一次重启真正的第一条错误已经被刷掉了。这篇文章不背命令表而是搭一条排障证据链。遇到容器起不来时先判断它停在哪一层再使用对应命令验证。文中示例是我为排障流程重新设计的实验场景不引用其他文章内容。命令以 Linux/Docker 通用写法为主涉及宿主机端口检查时同时给出 Windows PowerShell 用法。一、先别急着看日志先看容器状态1.1 “启动失败”其实至少有四种状态用户说“容器起不来”可能指下面任何一种情况状态现象优先检查Created容器创建了但主进程没真正启动启动参数、运行时错误Exited主进程已经退出退出码、日志、入口脚本Restarting主进程反复退出被重启策略拉起第一段日志、健康检查、依赖Running容器活着但业务访问不到监听地址、端口、网络、防火墙所以第一条命令不是docker logs而是dockerps-a只看目标容器dockerps-a--filternameapi-demo进一步查看状态dockerinspect api-demo\--formatstatus{{.State.Status}} exit{{.State.ExitCode}} error{{.State.Error}}示例statusexited exit1 error这个结果至少说明两件事Docker 已经成功创建并启动过容器容器里的主进程自己以状态码 1 退出。这时继续研究宿主机防火墙没有意义应先看进程为什么退出。1.2 退出码能告诉我们多少常见退出码可以作为方向但不能当作最终结论退出码常见含义0进程正常结束但服务型容器不应该立刻结束1应用通用错误126命令存在但不可执行127找不到命令137收到 SIGKILL可能是 OOM 或人工强制停止139段错误143收到 SIGTERM常见于正常停止流程查看完整状态dockerinspect api-demo--format{{json .State}}如果有jqdockerinspect api-demo--format{{json .State}}|jq重点字段包括Status Running ExitCode Error StartedAt FinishedAt OOMKilled HealthExitCode137时不要直接认定为 OOM应继续看dockerinspect api-demo--formatoom{{.State.OOMKilled}}如果OOMKilledfalse也可能是管理员执行了docker kill或者外部编排系统强制终止。状态码负责缩小范围不负责替我们下结论。二、建立一条固定的排障证据链我习惯按照下面顺序排查状态 → 退出码和错误 → 应用日志 → 启动配置 → 文件与权限 → 监听地址和端口 → 容器网络与依赖 → 健康检查 → 最小化复现这条顺序不是绝对的但能避免常见的跳跃网页打不开 → 怀疑 Docker 网络 → 重建 network → 清理镜像 → 最后发现应用根本没有启动排障时每一步都要留下证据。例如不要只说“端口应该没问题”而要给出dockerport api-demo ss-lntpcurl-vhttp://127.0.0.1:18080/health命令的结果能互相验证才算把这一层排除。三、日志先抓住第一次失败3.1 最常用的docker logsdockerlogs api-demo容器重启很多次时日志可能很长。先看末尾dockerlogs--tail100api-demo持续跟踪dockerlogs-f--tail50api-demo带时间戳dockerlogs-t--tail100api-demo只看最近十分钟dockerlogs--since10m api-demo如果容器处于 Restarting我更建议先关闭自动重启干扰dockerupdate--restartno api-demodockerstop api-demodockerstart-aapi-demodocker start -a会把当前启动过程直接附加到终端第一条错误更容易看到。这一组输出应该连着看ps -a确认容器状态inspect给出退出码和 OOM 标记logs保留应用报错。单独看到一个Exited (1)信息还不够。调完后再恢复策略dockerupdate--restartunless-stopped api-demo3.2 为什么有时docker logs是空的docker logs只能读取容器主进程写到标准输出和标准错误的内容。如果应用把日志写进/var/log/app/app.log而没有输出到 stdout/stderrDocker 日志自然为空。检查日志驱动dockerinspect api-demo--format{{.HostConfig.LogConfig.Type}}常见结果json-file local journald none如果是nonedocker logs不会返回应用日志。对容器应用更推荐把普通运行日志写到 stdout/stderr让日志收集系统统一处理。把日志只写在容器文件系统中容器删除后很容易一起丢失。3.3 入口脚本为什么经常让错误变得模糊例如 DockerfileCOPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT [docker-entrypoint.sh] CMD [node, server.js]脚本#!/bin/shset-eechopreparing configuration./prepare-config.sh$这里的$没有使用exec。主应用变成 shell 的子进程信号转发和退出状态处理可能出现问题。更好的写法#!/bin/shset-euechopreparing configuration2./prepare-config.shexec$exec会让目标程序替换 shell成为容器中的主进程。另外还要检查filedocker-entrypoint.sh如果脚本在 Windows 下编辑可能带 CRLF容器中出现/bin/sh^M: bad interpreter转换方式sed-is/\r$//docker-entrypoint.shchmodx docker-entrypoint.sh四、端口问题区分容器端口和宿主机端口4.1-p 18080:8080到底是什么意思dockerrun-p18080:8080 demo-api:1.0含义是宿主机 18080 → 容器 8080左边是宿主机端口右边是容器内部应用监听的端口。最常见的错误是应用实际监听 3000却映射到 8080dockerrun-p18080:8080 demo-api:1.0容器可以正常 Running但访问18080没有响应。检查镜像配置dockerimage inspect demo-api:1.0\--format{{json .Config.ExposedPorts}}检查容器端口绑定dockerport api-demo检查实际配置dockerinspect api-demo\--format{{json .NetworkSettings.Ports}}注意Dockerfile 中的EXPOSE 8080只是元数据和说明不会自动把端口发布到宿主机。4.2 宿主机端口已经被占用Linuxss-lntp|grep:8080或者lsof-iTCP:8080-sTCP:LISTENWindows PowerShellGet-NetTCPConnection-LocalPort 8080-State Listen继续查进程Get-Process-Id 18724如果端口被占用不要第一反应杀进程。先判断那个进程是不是正常服务。最安全的验证方式是换一个宿主机端口dockerrun--rm-p18080:8080 demo-api:1.0端口冲突时不要先改一串配置。先证明 8080 被谁监听再换 18080 验证镜像内部服务。两步能把“宿主机端口问题”和“容器内服务问题”拆开。4.3 端口映射正确为什么还是访问不到检查应用在容器中监听的地址。错误示例server.listen(8080,127.0.0.1);这只监听容器自己的回环地址。端口发布流量进入容器后不一定能到达这个监听。容器服务一般应监听server.listen(8080,0.0.0.0);127.0.0.1和0.0.0.0的区别发生在容器内部。前者只接收容器回环流量后者才会接收进入容器网卡的连接。宿主机端口映射写对了并不能替应用修改监听地址。进入容器检查dockerexec-itapi-demosh如果镜像中有ssss-lntp或者直接从容器内部请求wget-qO- http://127.0.0.1:8080/health判断方式结果方向容器内也访问失败应用未监听或应用异常容器内成功宿主机失败端口发布、防火墙或监听地址宿主机成功外部机器失败宿主机防火墙、云安全组、绑定地址4.4 不要随意把数据库端口发布到所有网卡dockerrun-p5432:5432 postgres默认会把端口发布到宿主机地址上具体暴露范围还受 Docker 和防火墙配置影响。如果只供本机访问可以明确绑定dockerrun-p127.0.0.1:5432:5432 postgres如果只供同一 Docker 网络中的容器访问甚至不需要-p。服务之间直接通过容器网络通信。能访问不等于应该暴露。排障时临时打开的端口解决后也要恢复最小暴露。五、挂载与权限容器里是哪个用户5.1 先看进程身份dockerexecapi-demoid输出可能是uid10001(app) gid10001(app) groups10001(app)再看挂载dockerinspect api-demo\--format{{range .Mounts}}{{println .Source - .Destination .Mode}}{{end}}假设宿主机目录/srv/api-data权限ls-ld/srv/api-data输出drwx------ 2 root root 4096 Jun 11 10:20 /srv/api-data容器内的 UID 10001 无法写入这个目录应用就可能报EACCES: permission deniedLinux 判断权限时看的是 UID/GID不是用户名长得是否一样。容器里的app和宿主机上的app即使同名只要数字 ID 不同对挂载目录的权限就可能完全不同。5.2 不要用chmod 777当最终方案chmod -R 777能快速判断“是不是权限问题”但不适合作为长期配置。更合理的处理方式包括sudochown-R10001:10001 /srv/api-datasudochmod-RurwX,go-rwx /srv/api-data或者让容器使用与宿主机文件一致的 UID/GID。Composeservices:api:image:demo-api:1.0user:1000:1000volumes:-./data:/app/data但这里也不能机械照抄。镜像内部如果依赖特定用户、HOME 目录或文件所有权强制切换 UID 可能引入新问题。正确顺序是确认容器进程 UID/GID确认挂载源目录所有者和权限确认应用需要读、写还是执行只授予实际需要的权限。5.3 文件存在容器为什么说找不到检查挂载的真实结果dockerinspect api-demo--format{{json .Mounts}}常见问题相对路径相对于当前执行目录而不是 Compose 文件所在目录宿主机文件不存在Docker 创建了同名目录把文件挂载到应用期望的目录上覆盖了镜像原内容Windows 路径没有共享给 Docker DesktopSELinux 阻止容器访问。例如dockerrun-v./config.json:/app/config.json demo-api:1.0如果./config.json不存在某些情况下宿主机会创建目录容器中/app/config.json也变成目录。启动前先检查test-f./config.jsonechookCompose 可以使用长语法让意图更清晰volumes:-type:bindsource:./config.jsontarget:/app/config.jsonread_only:true5.4 SELinux 场景在启用 SELinux 的 Linux 发行版上即使 Unix 权限看起来正确也可能被安全上下文阻止。可以检查getenforcels-Z/srv/api-dataDocker 绑定挂载可使用合适的标签选项例如:Z或:z但两者共享语义不同不能盲目添加。不要为了验证直接永久关闭 SELinux。可以先查审计日志确认是否真的由策略拒绝。六、环境变量与启动命令6.1 容器中到底拿到了哪些变量dockerinspect api-demo\--format{{range .Config.Env}}{{println .}}{{end}}或者进入容器dockerexecapi-demoenv|sort注意不要在公开日志中直接打印密码、Token 和数据库连接串。更安全的做法是只检查变量是否存在test-n${DATABASE_URL:-}\echoDATABASE_URL is set\||echoDATABASE_URL is missing6.2 Compose 变量替换和容器环境不是一回事Compose 文件services:api:image:demo-api:${APP_VERSION}environment:APP_ENV:${APP_ENV:-production}这里至少有两次处理Compose 在宿主机读取.env或当前环境替换${...}替换后的APP_ENV才进入容器。查看最终配置dockercompose config这是排查 Compose 变量最有用的命令之一。它能让我们看到变量替换后的镜像名最终端口最终挂载路径合并后的多个 Compose 文件网络和环境配置。如果docker compose up的行为和想象不同先保存dockercompose configresolved-compose.yml不要只盯着原始 YAML 猜。七、网络问题先确认“谁访问谁”7.1 宿主机访问容器路径浏览器/宿主机进程 → 宿主机发布端口 → Docker 转发 → 容器监听端口检查命令dockerport api-democurl-vhttp://127.0.0.1:18080/healthdockerexecapi-demowget-qO- http://127.0.0.1:8080/health这三条分别验证端口配置、宿主机访问和容器内应用。7.2 一个容器访问另一个容器Composeservices:api:image:demo-api:1.0environment:DATABASE_HOST:dbdb:image:postgres:17api应该访问db:5432而不是localhost:5432容器中的localhost指容器自己不是宿主机也不是另一个容器。这类错误常出现在把本地开发配置原样搬进 Compose 时。API 容器访问数据库要使用 Compose 服务名db写成localhost请求只会回到 API 容器自己。检查网络dockernetworklsdockerinspect api-demo--format{{json .NetworkSettings.Networks}}dockerinspect db--format{{json .NetworkSettings.Networks}}两个容器要通过服务名通信必须处在可互通的用户定义网络中。测试 DNSdockerexecapi-demo getent hosts db测试端口dockerexecapi-demosh-cnc -vz db 5432但精简镜像中可能没有getent、nc、curl。不要为了排障把一堆工具永久装进生产镜像可以临时启动调试容器进入同一个网络dockerrun--rm-it\--networkdemo_default\nicolaka/netshoot如果环境不允许使用额外镜像也可以使用已有基础镜像做最小测试。7.3depends_on不等于依赖服务已经就绪Compose 中depends_on:-db主要表达启动顺序不代表数据库已经完成初始化并可以接受连接。更稳妥的处理为数据库配置健康检查应用连接失败时做有限退避重试把“容器启动”与“服务就绪”分开不要用固定sleep 30当长期方案。固定等待时间在自己电脑上可能够换到慢磁盘或 CI 环境就不够设置太长又会浪费每次启动时间。八、健康检查失败但应用明明能访问8.1 HEALTHCHECK 在哪个环境执行健康检查命令在容器内部执行。DockerfileHEALTHCHECK --interval10s --timeout3s --retries3 \ CMD curl -f http://localhost:8080/health || exit 1如果镜像里没有curl每次检查都会返回 127。健康检查不是从宿主机替你访问服务而是在容器里执行命令。因此命令是否存在、工作目录、环境变量和容器内端口都要成立。浏览器能打开页面不代表这条检查命令一定能运行。查看结果dockerinspect api-demo\--format{{json .State.Health}}手动执行同一条命令dockerexecapi-demosh-c\curl -f http://localhost:8080/health如果提示curl: not found问题不在应用。可以改成镜像已有的工具或者在应用中提供轻量自检命令HEALTHCHECK CMD [node, healthcheck.js]8.2 健康检查不要检查太多东西一个/health接口同时检查数据库Redis第三方支付邮件服务对象存储只要任何一个外部服务短暂抖动容器就变成 unhealthy甚至被编排系统重启。建议区分检查目的liveness进程是否还能工作失败时可考虑重启readiness当前是否适合接收流量startup启动是否完成给慢启动程序宽限时间Docker 原生HEALTHCHECK只有一套状态复杂系统要在应用和编排层继续区分。8.3 启动宽限时间应用初始化需要 40 秒但健康检查从第 5 秒开始每 3 次失败就 unhealthy。可以设置HEALTHCHECK \ --interval10s \ --timeout3s \ --start-period45s \ --retries3 \ CMD [node, healthcheck.js]start-period不是越长越好而是要覆盖正常启动的合理上限。九、最小化复现比反复改配置有效9.1 从最小命令开始复杂 Compose 启动失败时可以先绕开编排文件dockerrun--rmdemo-api:1.0再逐项加入dockerrun--rm\-eAPP_ENVproduction\demo-api:1.0然后加入端口dockerrun--rm\-eAPP_ENVproduction\-p18080:8080\demo-api:1.0最后加入挂载和网络。当某一步开始失败新增的那一组配置就是优先检查对象。9.2 覆盖入口命令进入镜像容器一启动就退出无法docker exec可以覆盖入口dockerrun--rm-it\--entrypointsh\demo-api:1.0进入后检查pwdls-laidenv|sortnode--versionnodeserver.js这能区分文件没复制进去工作目录不对运行用户无权限依赖缺失应用本身启动失败。9.3 比较镜像默认配置和容器实际配置镜像dockerimage inspect demo-api:1.0\--formatuser{{.Config.User}} workdir{{.Config.WorkingDir}} entry{{json .Config.Entrypoint}} cmd{{json .Config.Cmd}}容器dockerinspect api-demo\--formatuser{{.Config.User}} workdir{{.Config.WorkingDir}} entry{{json .Config.Entrypoint}} cmd{{json .Config.Cmd}}如果两者不同说明运行参数或 Compose 覆盖了镜像设置。把前面的检查串起来最终应该得到一组能互相印证的输出容器状态说明它停在哪inspect给出退出码日志保留第一条可执行的错误。总结Docker 排障不是记住越多命令越好而是每次都知道当前在验证哪一层。排查的顺序其实很简单先看状态Exited / Restarting / Running / unhealthy再看退出码和第一段日志然后检查启动命令、环境变量和 Compose 解析结果。权限问题用 UID/GID 和真实挂载信息来判断。端口要分清三个层宿主机端口、容器端口、应用监听地址。容器间访问和宿主机访问容器也要分开看。复杂配置失败就做最小化复现修好以后别只记“重启好了”把验证命令也留下来。先看状态确认是 Exited、Restarting、Running 还是 unhealthy再看退出码和第一段日志检查最终启动命令、环境变量和 Compose 解析结果用 UID/GID 和真实挂载信息判断权限分清宿主机端口、容器端口与应用监听地址分清宿主机访问容器和容器访问容器手动执行健康检查命令复杂配置失败时做最小化复现修好以后保留验证命令不要只留下“重启好了”。容器显示 Running 只证明主进程没挂业务能不能用还得看端口、依赖和健康检查。排查的目标不是“这次起来了”是找到能复现、能解释、能验证的根因。参考资料Docker Docs查看容器日志https://docs.docker.com/reference/cli/docker/container/logs/Docker Docs发布和暴露端口https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/Docker DocsPort publishing and mappinghttps://docs.docker.com/engine/network/port-publishing/Docker DocsDockerfile HEALTHCHECKhttps://docs.docker.com/reference/dockerfile/#healthcheckDocker DocsBind mountshttps://docs.docker.com/engine/storage/bind-mounts/Docker DocsCompose 配置解析https://docs.docker.com/reference/cli/docker/compose/config/