跳到主要内容

Docker 故障诊断

排障不是随机尝试命令。稳定的方法是:先保留现场,再从状态和日志收集证据,缩小到容器、应用、网络、存储或宿主机,最后修改并验证。

完成后,你将能够:

  • 使用一套固定顺序采集 Docker 故障信息;
  • 根据容器状态和退出码判断方向;
  • 排查端口、DNS、挂载、内存和磁盘问题;
  • 使用临时工具容器诊断精简镜像;
  • 在清理前保留必要现场。

1. 通用排障流程

遇到问题时按以下顺序执行:

  1. 记录现象:何时开始、影响哪个容器、最近改了什么;
  2. 查看状态:容器是否存在、运行、重启或退出;
  3. 查看日志和退出码:应用自己通常已经给出原因;
  4. 检查配置:启动命令、环境变量、挂载、网络、端口、健康检查;
  5. 检查宿主机:Engine、磁盘、inode、内存、DNS、防火墙;
  6. 最小化复现:使用前台运行或临时工具容器验证假设;
  7. 修复并复测:确认原症状消失,同时避免引入新问题。
不要一上来就删除

docker rmcompose downprune 可能删除退出码、日志、可写层和网络关系。先采集证据和备份数据,再决定是否重建或清理。

2. 六条基础采集命令

<container> 替换为实际容器名或 ID:

# 1. 查看所有容器,包括已退出和不断重启的容器
docker ps -a --no-trunc

# 2. 查看目标容器最近 200 行日志和时间戳
docker logs --timestamps --tail 200 <container>

# 3. 提取状态、退出码、错误、OOM 和重启次数
docker inspect <container> \
--format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}} restarts={{.RestartCount}}'

# 4. 查看端口、网络和挂载的完整配置
docker inspect <container>

# 5. 获取当前资源快照,寻找 CPU 或内存异常
docker stats --no-stream <container>

# 6. 查看镜像、容器、卷和构建缓存占用
docker system df --verbose

如果是 Compose 项目:

# 查看服务状态,包括退出码和健康状态
docker compose ps --all

# 按时间顺序查看项目所有服务最近日志
docker compose logs --timestamps --tail 200

# 验证 Compose 解析后的配置和变量值
docker compose config

3. 看懂状态和退出码

状态或退出码常见含义下一步
Created已创建但未启动,或启动前失败查看 .State.Error 和挂载配置
Restarting进程不断退出,重启策略又拉起查看日志、退出码和启动命令
Exited (0)主进程正常结束判断应用是否本来就应长期运行
Exited (1)应用通用错误查看应用日志和配置
Exited (126)命令存在但不能执行检查权限、架构和挂载选项
Exited (127)找不到命令检查镜像内容、路径和入口命令
Exited (137)收到 SIGKILL,常见于 OOM 或强制停止检查 OOMKilled、内核日志和内存限制
Exited (143)收到 SIGTERM 后退出常见于正常停止或部署替换
unhealthy主进程在运行,但健康检查失败查看健康检查日志和应用端点

退出码只是线索,不是最终结论。例如 137 也可能来自 docker kill,必须结合 OOMKilled 和宿主机日志判断。

4. 实验一:容器立即退出

主动创建一个失败容器:

# shell 输出错误信息并以退出码 42 结束
docker run --name exit-demo \
alpine:3.20 \
sh -c 'echo "configuration missing" >&2; exit 42'

# 容器不在 docker ps 默认结果中,因为它已经退出
docker ps -a --filter name=exit-demo

# 日志给出应用层错误
docker logs exit-demo

# 精确读取退出码,预期为 42
docker inspect exit-demo \
--format 'status={{.State.Status}} exit={{.State.ExitCode}}'

# 保留证据检查完毕后删除实验容器
docker rm exit-demo

真实场景中的常见原因:入口命令写错、配置缺失、文件权限不足、依赖服务不可用、程序本应为一次性任务。可以临时覆盖入口命令进入 shell,但不要无依据地修改镜像:

# 忽略镜像原入口并启动 shell,仅用于镜像确实包含 sh 的情况
docker run --rm --interactive --tty \
--entrypoint sh \
<image>:<tag>

5. 实验二:端口无法访问

先复现“容器运行但宿主机无法访问”:

# Nginx 正常运行,但没有使用 --publish
docker run --detach \
--name port-demo \
nginx:1.27-alpine

# 输出为空,说明没有宿主机端口映射
docker port port-demo

# 容器内部访问成功,证明应用本身正在监听 80 端口
docker exec port-demo \
wget -q -S -O /dev/null http://127.0.0.1:80

运行中的容器不能通过简单命令追加端口映射,需要按正确配置重建:

# 删除配置错误的实验容器
docker rm --force port-demo

# 使用正确映射重新创建
docker run --detach \
--name port-demo \
--publish 127.0.0.1:8080:80 \
nginx:1.27-alpine

# 从宿主机验证映射后的端口
curl --head http://127.0.0.1:8080

# 清理实验容器
docker rm --force port-demo

如果仍不通,依次检查:

# 容器是否运行,PORTS 列是否符合预期
docker ps -a --filter name=<container>

# 应用日志是否显示绑定失败或启动错误
docker logs --tail 100 <container>

# 查询 Docker 实际发布的地址和端口
docker port <container>

# macOS/Linux 查看宿主机对应端口的监听者
lsof -nP -iTCP:8080 -sTCP:LISTEN

还要确认应用在容器内监听 0.0.0.0,而不是只监听容器自身的 127.0.0.1;远程访问则继续检查宿主机防火墙、云安全组和上游路由。

6. 实验三:服务名无法解析

两个容器必须位于同一自定义网络,才能稳定地按名称通信:

# 创建网络并启动目标服务
docker network create troubleshoot-net
docker run --detach \
--name dns-web \
--network troubleshoot-net \
nginx:1.27-alpine

# 未加入该网络的临时容器无法解析 dns-web;失败是预期现象
docker run --rm alpine:3.20 \
sh -c 'getent hosts dns-web || echo "name cannot be resolved"'

# 加入同一网络后,Docker DNS 可以解析容器名
docker run --rm \
--network troubleshoot-net \
alpine:3.20 \
getent hosts dns-web

# 查看网络实际连接的容器
docker network inspect troubleshoot-net

# 清理实验环境
docker rm --force dns-web
docker network rm troubleshoot-net

应用连接串应写 dns-web:80 或 Compose 服务名,不要写另一个容器的 localhost,也不要保存容器 IP。

7. OOM 和资源问题

先查看 Docker 记录:

# 检查是否被 OOM Kill、退出码和配置的内存上限
docker inspect <container> \
--format 'oom={{.State.OOMKilled}} exit={{.State.ExitCode}} memory_limit={{.HostConfig.Memory}}'

# 查看存活容器当前内存使用与上限
docker stats --no-stream <container>

Linux 宿主机继续检查内核和服务日志:

# 搜索内核日志中的 OOM Killer 记录
sudo dmesg -T | grep -i -E 'out of memory|killed process|oom'

# 查看最近 30 分钟 Docker Engine 日志
sudo journalctl -u docker --since '30 minutes ago' --no-pager

# 查看宿主机总体内存和 swap 使用
free -h

处理方向包括:修复内存泄漏、降低并发或缓存、为应用设置合理堆上限、根据压测调整容器限额。不要只看一次 docker stats 就无限提高上限,这可能把单容器问题扩大到整台宿主机。

8. 健康检查失败

# 输出健康状态和每次探测的退出码、时间、错误信息
docker inspect <container> \
--format '{{json .State.Health}}'

# 查看镜像或运行配置中的健康检查定义
docker inspect <container> \
--format '{{json .Config.Healthcheck}}'

# 在容器内手动运行同等探测,观察完整错误
docker exec <container> \
wget -S -O /dev/null http://127.0.0.1:8080/health

常见原因是路径、端口或协议错误,镜像内缺少检查命令,启动宽限期太短,或应用依赖尚未就绪。健康检查命令应快速、稳定,并检查真正能代表服务就绪的端点。

9. 挂载与权限问题

# 查看挂载类型、宿主来源、容器目标和读写模式
docker inspect <container> \
--format '{{json .Mounts}}'

# 查看容器实际运行 UID/GID
docker exec <container> id

# 查看目标目录权限;镜像必须包含 ls
docker exec <container> \
ls -ld /path/to/mount

# 在 Linux 宿主机查看 bind mount 源目录权限
ls -ld /host/source/path

逐项确认:源路径是否正确、文件还是目录、是否只读、UID/GID 是否匹配、SELinux 标签是否允许。不要把 chmod -R 777 当作通用修复,它会扩大安全风险并掩盖真正的部署契约问题。

10. 磁盘或 inode 耗尽

# 查看 Docker 各类对象的可回收空间和详细列表
docker system df --verbose

# Linux 查看 Docker 数据目录所在分区容量
df -h /var/lib/docker

# inode 耗尽也会导致“磁盘有空间却无法写文件”
df -i /var/lib/docker

# 查看所有容器日志驱动和日志路径
docker inspect <container> \
--format 'driver={{.HostConfig.LogConfig.Type}} path={{.LogPath}}'

安全处理顺序:

  1. 找出增长来源:容器日志、应用数据、镜像还是构建缓存;
  2. 先控制继续增长,例如限制日志和异常重试;
  3. 备份并确认数据归属;
  4. 只删除明确无用的对象;
  5. 为容量、inode 和增长速率增加监控。

11. 镜像拉取失败

# 直接拉取并保留完整错误:可能是超时、鉴权、证书或不存在
docker pull nginx:1.27-alpine

# 查看 Engine 代理、Registry Mirrors 和 DNS 相关信息
docker info

# Linux 查看最近 Engine 日志中的拉取错误
sudo journalctl -u docker --since '15 minutes ago' --no-pager

# 检查到 Docker Hub Registry 的 TLS 和 HTTP 连通性
curl -I https://registry-1.docker.io/v2/

401 Unauthorized 对 Docker Hub /v2/ 探测可以是正常响应,说明 TLS 和网络已建立;超时、DNS 失败或证书错误才指向网络链路。私有仓库还需检查登录凭据、镜像路径、证书信任和代理 NO_PROXY

12. 精简镜像没有排障工具

不要为了排障永久修改业务镜像。可以让临时工具容器共享目标网络命名空间:

# netshoot 包含 curl、dig、tcpdump、ss 等工具,并共享目标网络
docker run --rm --interactive --tty \
--network container:<container> \
nicolaka/netshoot

# 进入工具 shell 后检查监听端口和本机 HTTP
ss -lntup
curl -v http://127.0.0.1:<port>

或者只加入目标自定义网络:

# 在 app-net 中验证 db 名称能否解析
docker run --rm \
--network app-net \
alpine:3.20 \
getent hosts db

临时工具容器也拥有对应网络访问能力,只在排障期间运行,结束后立即删除。

13. 清理前检查

# 先列出待处理对象,不加 -f,让 Docker 请求确认
docker container prune

# 默认只删除 dangling 镜像,不删除所有未使用镜像
docker image prune

# 清理构建缓存前显示预计可回收空间并请求确认
docker builder prune

不要在没有备份和归属信息时执行 docker system prune -a --volumes。清理停止容器前,至少保存:

  • docker inspect 输出;
  • 相关时间段日志;
  • Compose 配置和环境变量来源;
  • 必要的容器可写层文件;
  • 卷备份或数据库逻辑备份;
  • 宿主机 Engine、内核和资源日志。

14. 快速决策表

现象第一证据常见根因
容器不见了docker ps -a已退出,或被 --rm 自动删除
容器不断重启日志、退出码、重启次数配置错误、依赖失败、OOM
宿主机访问失败docker port、容器内请求未发布、映射反了、应用未监听
容器间访问失败network inspect、DNS 测试不同网络、用了 localhost、名称错误
写文件失败Mountsid、目录权限只读挂载、UID/GID、SELinux、磁盘满
服务 unhealthy.State.Health.Log检查命令缺失、端点错误、启动太慢
退出码 137OOMKilled、内核日志内存不足、强制 kill
拉取失败pull 原始错误、Engine 日志DNS、代理、认证、证书、镜像名错误

下一篇提供按对象组织的命令速查表。遇到不熟悉的参数时,仍应先运行 docker <命令> --help 查看本机版本支持情况。