Docker容器排障不靠背命令:一套从宿主机到退出码的检查清单

Docker容器排障退出码
于 2026-10-01 05:07:38 修改
·本内容遵循CC 4.0 BY-SA版权协议

我搞了快十年容器运维,摸过的故障案例少说几百个,现在遇到 Docker 相关问题,反而不太着急上手敲命令。一直以来,排障的熟练度不取决于你背了多少命令,而是你对"先确认什么、再确认什么"有一套固定的反应。很多时候,大家一上来就去翻应用日志、改配置、重启容器,折腾半天发现是宿主机的磁盘满了,或者是容器时间不同步,这类基础问题根本不在于应用层。

所以这篇文章我把自己的固定动作分享出来。标题说"核对几个点",其实就是我的一套排障前置清单。它不能帮你解决所有问题,但能帮你把大部分伪故障挡在门外,真正把时间花在值得排查的事情上。

1. 资源与环境层面:先确认宿主机动没动、饿没饿

1.1 Docker 守护进程本身活着吗

这一步听起来像废话,但我确实见过太多人忽略它。容器起不来、网络不通、volumes 挂载不上,第一反应别去翻应用,先确认 docker daemon 的状态。

BASH
systemctl status docker

如果这一行显示 Active: active (running),再看一眼是不是有大量重启记录。很多情况下服务处于 activating (auto-restart) 状态,像打地鼠一样不断拉起又崩溃,这往往说明 dockerd 启动时某个环节没过,比如存储驱动加载失败、iptables 操作被拒绝、镜像存储目录权限异常。

进一步确认守护进程状态,还可以用:

BASH
docker info
docker version --format '{{.Server.Version}}'

docker info 如果输出了完整的系统信息、存储驱动、容器运行时,说明 daemon 至少活着,API 是通的。如果卡住不动或报 Cannot connect to the Docker daemon,那问题就出在守护进程这一层,不用急着去看应用日志。

我碰到过一种情况是系统刚重启完,docker 服务设置的是 disabled,直接没拉起。这个问题用 journalctl -u docker --no-pager -n 50 看日志特别快,一眼能看到是正常停掉还是 crash 了。

1.2 宿主机的 CPU、内存、磁盘和水位线

容器跑不动,很多情况下不是容器本身的问题,是宿主机没资源了。

BASH
free -h
df -h
df -i

这三个命令是我每次排障必敲的:

  • free -h 看内存是否耗尽,尤其是 swap 是否被大量占用。宿主机内存紧的时候,容器里的进程会变得异常缓慢,表现为超时、连接拒绝,但应用日志里没有报错。
  • df -h 看磁盘。Docker 的 overlay2 存储驱动和日志文件(json-file)都非常能吃空间。根分区满了以后会有一系列连锁反应,比如容器无法创建、镜像无法拉取、数据库写不进数据。
  • df -i 看 inode。这是个冷门坑,磁盘显示有空间,但 inode 用完了,照样本地文件创建失败。/var/lib/docker 目录下小文件太多时,很容易踩这个。

这三个指标对不上,后面排查再深都是白费力气。

1.3 系统时间同步:容器错乱的第一大隐形坑

容器时间跟宿主机走,宿主机跟硬件时间或者 NTP 走。一旦宿主机时间漂移,服务请求校验失败、日志时间戳乱套、定时任务乱触发,排障方向会被带偏。

BASH
date
date -u

检查时间和 UTC 时间,顺带看一眼时间同步服务是否正常。

BASH
timedatectl status

System clock synchronized: yes 和 NTP service: active 这两个状态要同时满足。如果显示 NTP 服务没起来,容器内的服务调用外部 HTTPS API 时大概率会挂——证书有效期校验直接失败,表现却是"网络不通",非常迷惑人。

这个点我吃过一次大亏。有一次线上环境所有容器间的双向 TLS 握手都在报错,查了证书有效期、SAN、CA 链,折腾了一晚上,最后发现宿主机时间慢了整整八小时。从那之后,时间同步检查永远排在我的前三步。

2. 镜像与容器状态机:看懂退出码和生命周期比啥都强

2.1 容器状态总共就那么几种,先定位它现在处于哪个阶段

排查容器问题的关键在于先弄清楚容器处于哪个生命周期状态。docker ps 只能看到 running 和 exited 两类,但容器状态有六种:

状态 含义 典型原因
created 已创建,未启动 只是 docker create 了,没 start
running 正在运行 正常
paused 已暂停 执行了 docker pause
restarting 正在重启 启动失败,处于重启策略回旋中
exited 已退出 启动失败,或内存 OOM,或进程正常结束
dead 僵死 极其异常,通常需要强制清理

restarting 和 exited 是排障中最常见的两个状态,两者的排查方向完全不同。在 restarting 状态下的容器,要用 docker inspect 看它的 RestartCount 和 State.Status:

BASH
docker inspect <container_id> --format '{{.RestartCount}}'

如果 RestartCount 不断变大,说明容器在启动脚本阶段持续失败。这时重点看日志,而不是怀疑应用内部业务逻辑。启动即崩溃的问题,大多出现在入口脚本、环境变量缺失、配置文件解析失败上。

2.2 退出码是容器递给你的一张纸条

容器退出码是整个排查链条里信息密度最高的单个字段。

BASH
docker inspect <container_id> --format '{{.State.ExitCode}}'

常见退出码速记:

  • 0:正常退出,进程主动结束了生命周期。如果是服务类容器一直退出,要检查是不是前台运行模式问题,很多镜像需要 -it 或者关联 TTY 保持前台运行。
  • 1:通用错误。程序自身逻辑失败或启动检查没通过,极需要结合日志继续查。
  • 2:参数类错误或程序内部错误,常见于解释器/运行时解析出错。
  • 127:命令找不到,容器启动脚本里某个二进制不存在,常见于 entrypoint 写错路径或镜像缺少依赖。
  • 137:被 SIGKILL 杀掉,最常见的是 OOM Kill。这个退码背后有两种情况:宿主机内存耗尽,或容器超过了 memory 限制。
  • 139:段错误,程序自身崩溃,通常是 C 扩展、底层库问题。
  • 143:被 SIGTERM 终结,一般是 docker stop 超时后系统升级信号。

中途我处理过一个经典案例:一个 Go 服务容器每三分钟退出一次,ExitCode 是 137。当时也很奇怪,明明容器没有被设置 --memory 限制,但仔细排查发现,宿主机可用内存不足,内核 OOM 机制直接随机挑进程杀掉,结果每次杀的都是这个容器进程。加了 --memory 限制后再看退出码,才真正定位到应用内存泄漏上了。

2.3 镜像本身:Tag、Digest 和语言写死

跑不起来还有一个常在盲区里的事:镜像 Tag 是否对得上。团队里最常见的是 latest 标签被更新了,某次重新 pull 之后行为跟以前完全不同。

BASH
docker inspect <image_id> --format '{{.RepoDigests}}'

对比 RepoDigest 比看 Tag 靠谱得多。生产环境我强烈建议直接 pin 摘要:

BASH
docker pull myrepo/app@sha256:abcdef123456...

这只锁定具体镜像内容,不依赖 Tag 变化。

另外很多 Java、Node 应用存在语言运行时版本兼容问题。Java 17 的 class 文件在 Java 11 镜像里跑不起来,Node 18 的多阶段构建产物丢到 Node 16 基础镜像,有些原生模块会直接启动失败。这类问题在 docker build 阶段不会报错,要到运行阶段才会暴露。

3. 网络类问题排查:从端口、链路到 DNS,三个最容易走眼的点

容器网络是排障中范畴最大也最容易晕的一个层。我按性价比排序理出三个固定动作:端口是否发布、网络模式是否匹配、DNS 是否解析。

3.1 端口发布:容器里听着,不代表宿主机连着

容器内服务在 8080 端口监听,那是容器网络的地址,宿主机不能直接 localhost:8080 访问。要宿主机访问,必须做端口发布:

BASH
docker run -p 8080:8080 myapp

形式是 -p 宿主机端口:容器端口。反过来容易被人忽略:EXPOSE 8080 只是镜像里的元数据声明,不产生任何实际发布行为。很多人读 Dockerfile 里写了 EXPOSE 就以为跑起来就能从外部访问,这是很典型的误解。

验证端口到底有没有发布出去,用:

BASH
docker port <container_id>

输出什么也没有,说明没发布。也可以用:

BASH
ss -tlnp | grep 8080

看宿主机上有没有对应监听。本地访问不了容器服务,先走这一步,别急着查应用日志。

3.2 网络模式:bridge、host、none 与自定义网络

Docker 的默认网络模式是 bridge。bridge 网络的特点就是容器有独立 IP,通过 NAT 访问外网。host 模式则是容器直接共享宿主机网络栈,好处是性能好、端口不用发布,坏处是网络隔离基本消失。

排障时我常让同事做这个自检:如果容器用了 --network host,但代码里监听的是 127.0.0.1,那么外部即使通过宿主机 IP 也访问不到服务。因为 host 模式下端口存在宿主机的网络栈里,监听地址就是容器进程自己绑定的地址。所以 code 里 localhost 的监听,就只对宿主机本机生效。

容器间互联场景下,不建自定义网络直接用默认 bridge,会发现 容器名 做 DNS 解析不通。默认 bridge 虽然支持容器间通过 IP 通信,但不支持 --link 之外的名称解析。创建自定义网络后同一网络下的容器才能直接用容器名互访:

BASH
docker network create app-net
docker run --network app-net --name api-server myapi
docker run --network app-net --name web-app myweb

这样 web-app 里直接访问 http://api-server:8080 就能通。我们团队为了避免"在测试环境能跑、换网络就挂"的问题,在 compose 文件里永远显式声明网络名。

3.3 DNS 与代理链路故障的伪装

容器内域名解析不通,表现起来很像外部服务挂了或者网络断了。先手动进容器内看一下:

BASH
docker exec -it <container_id> nslookup example.com

如果容器里没有 nslookup,也可以用 getent hosts:

BASH
docker exec -it <container_id> getent hosts example.com

解析不了就查 daemon 的 DNS 配置。默认情况下 Docker 会继承宿主机 /etc/resolv.conf。宿主机用 systemd-resolved 时,/etc/resolv.conf 往往指向 127.0.0.53,这个地址在容器网络里不一定可达,结果是容器内 DNS 全挂。

解决办法是在启动命令里显式指定 DNS:

BASH
docker run --dns 8.8.8.8 --dns 114.114.114.114 myapp

或者修改 daemon.json,全局加上 "dns" 配置后重启 docker。

还有一种情况是容器内访问外网要通过代理,但环境变量没传进去。HTTP_PROXY、HTTPS_PROXY、NO_PROXY 这三个变量必须显式传给容器,不然 curl 应用侧看到的响应是连接超时。排到这一步时先别怀疑服务端,得看代理链路有没有走通。

4. 资源限制与应用崩溃:为什么容器突然没了

4.1 OOM Kill:从 137 退出码开始追

容器退出码 137 几乎百分之百指向被 SIGKILL 杀掉,其中又绝大多数是和内存相关。但要分清楚两个方向:容器自身超过了限制被 kill,还是宿主机整体内存耗尽被内核找上。

先看容器当前限制:

BASH
docker inspect <container_id> --format '{{.HostConfig.Memory}}'

输出 0 表示无限制。如果这里设置了 512MB,而应用实际需要 1GB,那 OOM 就很正常了。

然后看宿主机层面:

BASH
dmesg -T | grep -i 'oom\|killed process' | tail -20

dmesg 能找到内核 OOM killer 的记录。如果出现了 oom-kill 且带容器进程 PID,说明确凿。这一步能直接区分是"容器限制"还是"宿主机资源不足"。

4.2 文件句柄和线程数:两个非常隐蔽的上限

内存和 CPU 好理解,文件句柄和线程数限制属于那种"不逼到墙角根本想不起来"的问题。

容器内 ulimit 并非默认无限:

BASH
docker run --ulimit nofile=65535:65535 myapp

高并发服务默认 1024 的 nofile 很快就撑爆了。表现是应用日志里疯狂报 too many open files,或者 EMFILE 错误。查宿主机的整体句柄使用率,用:

BASH
cat /proc/sys/fs/file-nr

三个数字分别代表已分配、已用、最大。如果第二个数接近第三个数,那是全宿主机层面的句柄瓶颈。

线程数限制同理,--ulimit nproc 或系统全局 kernel.threads-max 不足时,进程会莫名其妙起新线程失败。这类问题最难排查之处在于:它在应用日志里几乎不留痕迹,只在某些特定高并发场景下偶发。

4.3 写 Java 和 Node 应用时常犯的容器内内存盲区

Java 应用在容器里跑是内存重灾区。很多人会在启动脚本里写:

BASH
- Xmx1024m

同时容器限制设成了 1GB。这其实是两头踩线:JVM 堆就占了 1GB,还要加上 Metaspace、线程栈、JIT 编译区这些堆外内存,实际用量轻松超过 1GB。容器内存限制一触发,直接 OOM Kill。

我看到过大量 Java 服务在 Docker 里"偶发崩溃",ExitCode=137,但应用日志完全没异常。最终解决方案是把 -Xmx 降下来,保留 Heap 之外的 Headroom,或者干脆容器限制设成堆上限的 1.5 倍。

Node 也有类似问题,默认 Heap 上限老版本里是 1.4GB,新版 4GB 左右。对内存密集型服务得用 NODE_OPTIONS=--max-old-space-size=1024 来约束,不然容器限制和 V8 的分配逻辑会打架。

5. 数据持久化与权限边界:挂载卷是最能"藏雷"的地方

5.1 挂载目录权限:no permission 不是权限问题,是 UID 错位

容器内进程用指定用户运行,宿主机挂载目录的权限是宿主机的,两边默认不匹配。这是 Docker 数据卷最经典的坑。

BASH
docker run -v /host/data:/app/data myapp

宿主机 /host/data 属主是 root、权限 755,容器内进程是 uid=1000,那写入时铁定报 Permission denied。

处理方式有三种:

  1. 宿主机目录权限放宽松,chmod 777 或改属主:
BASH
chown 1000:1000 /host/data
  1. 启动容器时用 --user 指定 UID,和宿主机目录属主对齐。

  2. 用命名卷(named volume)代替 bind mount。Docker 初始化命名卷时会自动设置属主为镜像内声明的用户。这是最省事的方式。

我习惯在 compose 里用命名卷来避免这一系列问题。特别是数据库类容器,MySQL、Postgres 镜像都会在镜像内创建数据目录并设置 owner,用命名卷能把权限问题绕开大半。

5.2 文件所有权、SELinux 与 AppArmor

RedHat/CentOS/Fedora 系宿主机上跑容器的挂载访问问题,还有一层 SELinux 因素。宿主机开启了 SELinux,但挂载的目录没有对应的类型标签时,容器进程即使 UID 匹配了也会被 SELinux 拒之门外。

日志里能看到 avc: denied 记录:

BASH
ausearch -m avc -ts recent

回避方式有两个:临时加 --security-opt label=disable,或者给目录打标签:

BASH
chcon -Rt svirt_sandbox_file_t /host/data

Ubuntu/Debian 系的 AppArmor 类似。默认 docker 的 AppArmor profile 对某些写入路径有约束。应用跑起来说"我明明有权限为什么写不进去",有相当一部分是这类 LSM 模块在做拦截。排查时如果所有传统权限检查都正常,记得往这个方向看。

5.3 持久化数据掉皇帝的"枯井":长期运行容器与 named volume 的坑

有一种情况很隐蔽:容器用 -v /host/data:/app/data 挂载正常,但容器内服务是软链方式写数据的。比如 /app/logs 是软链指向 /var/log/app,而挂载点是 /app,那容器目录覆盖时会把软链一起覆盖掉,写出来的数据实际落在容器可写层,容器一删全没。

排查时要进容器内确认真实写入路径:

BASH
docker exec -it <container_id> ls -la /app/logs
docker exec -it <container_id> df -h /app/logs

看 df 的输出挂载点是 overlay 还是宿主机路径,一眼便知数据有没有真的落进卷里。这个问题坑过很多团队,因为所有东西看起来都正常,日志还在输出,只是重启后全部蒸发。

6. 日志定位技巧:从三千行日志里捞出关键线索

6.1 stdout 是容器日志的唯一通道

先给新手敲个警钟:Docker 里 docker logs 只能看到进程写到标准输出(stdout)和标准错误输出(stderr)的内容。应用如果写日志到文件而不输出到 stdout,docker logs 永远看不到内容。

很多镜像里的应用默认写文件日志,比如 Tomcat 写 catalina.out,Nginx 写 access.log。用 docker logs 查不到时,不要觉得是日志系统坏了,先确认应用日志有没有配置为控制台输出。通常在 Dockerfile 里会用软链或参数调整把日志重定向到 stdout:

BASH
ln -sf /dev/stdout /var/log/nginx/access.log

或启动命令里加上 2>&1。

6.2 docker logs 的用法不是只有 tail

BASH
docker logs -f --tail 200 <container_id>

这是我用得最多的组合。但有三个阶段要留意:

  • 第一阶段看启动日志:容器刚起来那几十秒的输出,能反映初始化是否成功。
  • 第二阶段看错误前后的日志:用 --since 定位时间窗口,比从头翻日志高效得多。
  • 第三阶段看崩溃前的最后输出:容器退出后还能 docker logs <container_id>,这是第一时间能拿到的现场。

6.3 docker events:实时观察处于 Restart 状态的容器

有些容器反复重启的时间间隙非常短,docker logs 还没来得及看,容器又退出了。这时用事件流来跟踪:

BASH
docker events --filter container=<container_id> --since 5m

可以看到这个容器的完整事件序列:start、die、start、die。配合 inspect 的 State.FinishedAt 和 State.StartedAt,能算出每次存活时长。

我见过一种模式:容器每 45 秒重启一次,日志里每次都是同一条配置文件解析失败的报错。没有事件流的辅助,眼睛盯着 docker logs 的滚动很容易漏掉这种周期性规律。

6.4 日志驱动调 json-file 时的空间问题

默认日志驱动是 json-file,日志文件无限制增长,最终吃满磁盘。这就是第一节提到的"磁盘满、容器挂"的直接来源之一。

daemon.json 里建议配置轮转:

JSON
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}

改完重启 Docker 只对新容器生效,已有容器需要重新创建或者配置 --log-opt 单独起。排障时看磁盘突然爆满,优先查 /var/lib/docker/containers/<id>/*-json.log 的大小,这块往往是最容易被忽略的日志大头。

7. 一套可复制的排障优先级,送给刚入门的人

理了这么多检查点,收敛成一套固定动作,是我现在遇到容器问题的第一反应:

  1. 看宿主机资源水位:free -h、df -h、df -i,顺手敲一下 date 确认时间同步。
  2. 确认 daemon 状态:docker info 能通,再看服务本身有没有频繁重启。
  3. 定位容器状态和退出码:docker ps -a 找容器,docker inspect 看 ExitCode、RestartCount、OOMKilled 标记。
  4. 查网络三件套:docker port 看发布、进容器 getent hosts 看 DNS、docker network ls 看模式。
  5. 翻日志看现场:docker logs --tail 200,如果有周期性重启规律就结合 docker events 一起看。

这套链路走下来,大多数"莫名其妙"的问题都能定位到某个具体的层次。剩下真正需要深入代码级排查的问题,也已经把环境变量、网络、权限、资源这些外围因素清理干净了,排查面会小很多。

我自己有个体会:排障时最大的浪费不是排查时间长,而是方向错了还不自知。前面那些基础检查只需要几分钟,却能把整个问题的定位从"大海捞针"收敛成"井底捞针"。每多确认一个底层变量正常,后面的定位就越精确。如果你现在正卡在某一个容器问题里,先冷静下来,按这个顺序过一遍,大概率能从日志海洋里捞出那条真正有价值的线索。

PHP docker容器内调取宿主机SHELL命令行
在PHP Docker容器内调取宿主机SHELL命令行是一个常见的技术挑战,尤其当PHP运行在容器内,而需要与位于宿主机上的shell交互时。面对这一需求,开发者提出了几种解决方案,但都各有优缺点。
weixin_38567873
2700
docker 在容器外执行某个容器内的某个命令操作
"通过脚本和`docker exec`命令在容器外执行容器内部命令,以及使用`nsenter`工具实现宿主机与容器间无缝调用shell命令"在 Docker 容器环境中,有时我们需要在不进入容器
weixin_38570296
4841
Docker容器访问宿主机网络的方法
### 总结综上所述,当Docker容器需要访问宿主机网络时,我们可以采用以下两种方法:1. 使用宿主机的IP地址替代localhost,并在容器配置中直接指定宿主机IP。
weixin_38678406
4818
Docker容器内部端口映射到外部宿主机端口的方法小结
**使用大写`-P`参数进行随机端口映射** 使用大写`-P`参数时,Docker会在宿主机上随机选择一个未使用的端口映射到容器内部端口。这种方式适用于不关心具体映射端口的场景。
莫等闲eason
8060
windows宿主机和docker容器设置挂载共享文件夹的步骤
"这篇教程详细介绍了如何在Windows宿主机上设置与Docker容器共享文件夹的步骤,使得容器内的程序可以方便地访问宿主机的数据,避免了繁琐的导入导出操作。"在Windows环境中,使用Doc
weixin_38739744
4353
在docker容器中调用和执行宿主机的docker操作
标题中的“在docker容器中调用和执行宿主机的docker操作”是指在Docker容器内部运行命令来控制宿主机上的Docker服务。
weixin_38704011
923
docker cp:从容器复制文件到宿主机,从宿主机复制文件到容器
在学习的过程中遇到这个问题,创建容器的时候没有挂载宿主机目录,导致在宿主机与容器之间无法进行文件共享,于是就想到如何从容器复制文件到宿主机或者从宿主机复制文件到容器。方法虽然简单,记录在博客中分享给大
weixin_38658982
5294
实现跨宿主机多docker容器间数据共享
### 实现跨宿主机多Docker容器间的数据共享#### 概述本文将详细介绍如何在不同的宿主机之间实现多个Docker容器之间的数据共享。
code飞翔
2251
网络排障docker镜像
本文介绍了网络排障中常用的方法和工具,包括使用`docker inspect`命令检查容器网络设置,`ping`命令测试网络连通性,`telnet`或`nc`命令检查端口访问,以及`ifconfig`或`ip addr`命令查看网络配置。同时,推荐了`nicolaka/netshoot`和`praqma/network-multitool`等预构建的Docker镜像,这些镜像集成了多种网络工具,便于进行网络排障和诊断。
2301_77596215
Java面试短期突击:从八股文到场景题的最小复习闭环
本文针对2-3周Java面试突击场景,提出以高频考点驱动的最小复习闭环方法:聚焦JVM(运行时数据区、参数、OOM排查)、并发编程(原子性/可见性/有序性、线程池、JMM)、MySQL(索引/B+树、事务/MVCC、锁机制)和Spring(三级缓存、循环依赖)四大核心模块;强调用“能讲两分钟”检验理解深度,通过场景题链路(现象→输入→环境→日志→边界)训练工程思维,并借助AI陪练但不替代判断,最终建立知识树、故障库与表达脚本实现高效反馈。
weixin_34101229
336