Docker 基础体系 · 第 19/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 故障排查:启动失败、网络、磁盘、OOM、构建和 Daemon
Docker 故障排查的难点,不在于记住更多命令,而在于先判断故障发生在哪一层:客户端是否连接到了正确的 Daemon,Daemon 是否能创建容器,容器内的主进程是否成功启动,网络命名空间和 DNS 是否正确,存储和内存是否满足约束,最后才是应用本身的错误。
本文以运行在 Linux 上的现代 Docker Engine、BuildKit 和 Compose 为范围。Docker Desktop、Windows 容器、Kubernetes CRI、Swarm 集群控制面有额外差异,不能直接套用所有路径和命令。
一、先建立故障模型:命令、Daemon、容器和应用是四个不同对象
一次常见的启动操作可以抽象为:
docker CLI
│ Unix socket / TCP / SSH
▼
dockerd
│ containerd / runc
▼
容器进程:PID 1
│
├── 文件系统、挂载、用户、环境变量
├── 网络命名空间、DNS、端口发布
└── cgroup 资源限制
这里有四类容易混淆的故障:
- 客户端故障:
docker命令不存在、当前用户没有访问 Unix socket 的权限、DOCKER_HOST或 Docker context 指向错误位置。 - Daemon 故障:
dockerd没启动、启动配置错误、存储驱动无法初始化、iptables 或 cgroup 初始化失败。 - 容器生命周期故障:镜像拉取成功,但容器创建、挂载、网络配置或启动命令失败。
- 应用故障:容器已经启动,PID 1 执行后立即退出,或者应用运行中崩溃、拒绝连接、健康检查失败。
因此,第一步应当确认客户端实际连接的对象:
docker context show
docker context ls
docker version
docker info
典型含义如下:
docker context show显示当前 context,例如default。docker version同时包含Client和Server部分;只有 Client 而没有 Server,通常表示 CLI 无法连接 Daemon。docker info能进一步显示存储驱动、容器数量、Cgroup、日志驱动和根目录等信息。
如果出现:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock
这还不能直接证明 Daemon 已停止。也可能是:
- 当前 context 指向了不存在的远程主机;
DOCKER_HOST覆盖了预期配置;- 当前用户没有访问
/var/run/docker.sock的权限; - Daemon 监听的是 TCP 或 SSH,而客户端仍连接 Unix socket。
可以先检查:
env | grep -E '^(DOCKER_HOST|DOCKER_CONTEXT)='
ls -l /var/run/docker.sock
docker context inspect "$(docker context show)"
Unix socket 的常见权限形态是:
srw-rw---- 1 root docker ... /var/run/docker.sock
把用户加入 docker 组等价于授予接近 root 的控制能力,因为 Docker API 可以创建特权容器、挂载宿主机根文件系统。因此这不是普通的“修复权限”操作,应按宿主机高权限变更处理。
二、统一证据顺序:先确定状态,再解释原因
1. 容器状态不等于应用健康状态
先列出所有容器,而不是只看运行中的容器:
docker ps -a
docker ps -a 中的状态至少要区分:
Created:容器对象已创建,但进程尚未启动;Up ...:容器主进程仍在运行;Exited (0):主进程正常退出;Exited (1):应用以错误码退出;Exited (137):常见于收到SIGKILL,可能是 OOM,也可能是人工执行了docker kill;Restarting:重启策略正在反复拉起失败的容器;Dead:Docker 无法正常清理或完成容器状态转换。
然后查看结构化状态:
docker inspect --format '
Name={{.Name}}
Status={{.State.Status}}
Running={{.State.Running}}
ExitCode={{.State.ExitCode}}
Error={{.State.Error}}
OOMKilled={{.State.OOMKilled}}
StartedAt={{.State.StartedAt}}
FinishedAt={{.State.FinishedAt}}
' CONTAINER
State.Error 表示 Docker 在创建或启动容器时记录的错误;ExitCode 和 OOMKilled 更偏向主进程已经启动后的结果。若容器处于 Created,通常还没有应用进程可供 docker logs 查看。
2. 日志、事件和内核日志记录的是不同时间线
docker logs --timestamps --tail=200 CONTAINER
docker events --since 10m
journalctl -u docker.service --since "10 minutes ago"
dmesg -T | grep -i -E 'oom|killed process|memory cgroup'
四类信息的边界不同:
docker logs读取容器进程的标准输出和标准错误,具体行为取决于 logging driver;docker events记录容器创建、启动、停止、销毁、网络和镜像等 Docker 事件;journalctl -u docker记录 Daemon 日志;dmesg记录 Linux 内核事件,例如 cgroup OOM、网卡、文件系统和内核级网络错误。
一个应用“没有日志”可能是应用没有写标准输出,也可能是容器在进程启动前就因挂载失败而退出,还可能是日志驱动把日志发送到远端后本地不再保留。不能仅凭 docker logs 为空判断应用没有执行。
三、启动失败:从创建、启动到应用退出逐步定位
1. 容器启动的实际阶段
一个容器大致经历以下阶段:
解析镜像和配置
↓
创建容器元数据
↓
准备 rootfs、可写层、挂载和网络
↓
创建 namespace 与 cgroup
↓
启动 OCI runtime 和 PID 1
↓
PID 1 执行 ENTRYPOINT/CMD
↓
应用运行或退出
不同阶段的错误表现不同:
- 镜像不存在:通常在创建容器前失败;
- bind mount 源路径不存在、权限不足或类型不匹配:容器可能无法启动;
exec format error:常见于镜像架构与宿主机架构不匹配,或入口脚本格式错误;permission denied:可能是入口文件不可执行、用户权限不足、挂载文件系统带noexec,或 SELinux/AppArmor 拒绝;- 应用立即退出:容器状态通常是
Exited,日志和退出码比 Daemon 日志更有价值。
先看镜像架构和容器配置:
docker image inspect IMAGE \
--format 'OS={{.Os}} ARCH={{.Architecture}} Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}'
docker inspect CONTAINER \
--format 'Path={{.Path}} Args={{json .Args}} User={{.Config.User}} WorkingDir={{.Config.WorkingDir}}'
ENTRYPOINT 和 CMD 的组合决定实际执行命令。若镜像定义:
ENTRYPOINT ["./server"]
CMD ["--port", "8080"]
实际命令通常是:
./server --port 8080
而 Compose 中的 command 通常会覆盖镜像的 CMD,不等价于无条件替换 ENTRYPOINT。把 shell 形式和 exec 形式混用,可能导致信号处理和参数展开不同:
# exec form,PID 1 直接是程序
ENTRYPOINT ["/usr/local/bin/server"]
# shell form,可能通过 /bin/sh -c 执行
ENTRYPOINT /usr/local/bin/server
生产中如果应用需要正确接收 SIGTERM,通常应让真正的应用进程成为 PID 1,或使用能够正确转发信号的 init 进程。否则 docker stop 发送的终止信号可能没有到达子进程,最终等待超时并发送 SIGKILL。
2. 用一次性容器区分镜像问题和编排问题
如果怀疑 Compose 的环境变量、挂载或网络配置,可以先脱离 Compose 测试镜像:
docker run --rm --entrypoint /bin/sh IMAGE -c '
id
pwd
ls -l /usr/local/bin/server
'
这个命令的前提是镜像中存在 /bin/sh。Alpine、Debian、Distroless 镜像的工具集合不同;Distroless 镜像可能没有 shell,此时不能把“没有 /bin/sh”误判为应用损坏。
检查 Compose 最终展开结果:
docker compose config
它会把多个 Compose 文件、变量替换和 profiles 展开成实际配置。重点核对:
image或build是否指向预期对象;command、entrypoint是否覆盖了镜像默认值;- 环境变量是否为空或被错误替换;
- 相对路径是相对于 Compose 文件项目目录解析,还是来自当前 shell;
- 端口、网络、卷和资源限制是否被覆盖。
3. 反例:容器“启动失败”其实是启动成功后正常退出
下面的容器会创建成功并立即退出:
docker run --name demo alpine sh -c 'echo started; exit 0'
docker ps -a --filter name=demo
docker logs demo
预期可以看到:
started
Exited (0)
容器的生命周期绑定 PID 1。docker run -d alpine 也不会自动保持运行,因为默认命令完成后容器就结束。调试时可以使用:
docker run --rm -it --entrypoint sh IMAGE
但这只证明 shell 能启动,不能证明原始入口程序、环境变量和挂载都正确。调试完成后仍需按实际入口命令复现。
四、网络故障:先区分容器内、Docker 网络和外部网络
1. Docker 网络的数据路径
Linux 容器通常拥有独立的 network namespace。连接到用户自定义 bridge 网络时,典型路径是:
容器进程
↓
容器 eth0
↓ veth pair
宿主机 bridge,例如 br-xxxx
↓
iptables/nftables 转发、NAT
↓
宿主机网卡 eth0
↓
外部网络
容器访问宿主机发布的端口、访问其他容器、访问互联网,走的路径并不相同:
- 容器到容器:通常通过 Docker bridge 和容器 IP;
- 宿主机到容器发布端口:通过端口发布规则;
- 容器到外部网络:通常经过宿主机 IP 转发和 SNAT;
- 外部到容器:只有发布端口等规则存在时,才会被 NAT 到容器。
查看网络对象和连接关系:
docker network ls
docker network inspect NETWORK
docker inspect CONTAINER --format '{{json .NetworkSettings.Networks}}'
2. EXPOSE 不等于端口发布
Dockerfile 中的:
EXPOSE 8080
主要是镜像元数据,表示应用预期监听 8080,并不会自动让宿主机可以访问。
运行时发布端口:
docker run -d --name web -p 127.0.0.1:8080:80 nginx
含义是:
宿主机 127.0.0.1:8080 → 容器 80
如果写成:
-p 8080:80
通常会绑定宿主机所有地址,而不只是回环地址。查看实际绑定:
docker port web
ss -lntp | grep 8080
若应用在容器内只监听 127.0.0.1:80,即使发布了端口,Docker 转发到容器 eth0 的流量也可能无法访问。容器内服务通常需要监听:
0.0.0.0:80
而不是仅监听容器内部回环地址。诊断可以在容器内执行:
docker exec web ss -lntp
前提是镜像中有 ss;也可以使用临时诊断容器加入目标网络命名空间进行测试。
3. DNS:服务名解析和端口连接是两件事
用户自定义网络通常提供 Docker 内置 DNS。Compose 中,同一网络上的服务可以使用服务名解析:
services:
api:
image: example/api
db:
image: postgres:16
api 容器访问数据库应使用:
db:5432
而不是:
localhost:5432
因为容器内的 localhost 指向当前容器自身。db 解析到的是数据库容器在该网络上的地址,连接使用的是数据库容器端口,不是宿主机发布端口。
诊断:
docker exec api getent hosts db
docker exec api cat /etc/resolv.conf
docker exec api sh -c 'nc -vz db 5432'
getent、nc 不一定存在。可以运行临时工具容器:
docker run --rm --network container:api nicolaka/netshoot \
sh -c 'getent hosts db && nc -vz db 5432'
--network container:api 让临时容器共享 api 的网络命名空间;它会看到相同的接口、路由和 DNS 配置。该操作只适合诊断,镜像来源和安全性应符合生产环境要求。
常见误区包括:
ping失败不等于 TCP 服务不可用,镜像可能没有ping,网络也可能过滤 ICMP;- DNS 能解析不等于目标端口有监听;
- TCP 连接成功不等于应用协议正确,HTTP、TLS、认证仍可能失败;
- Compose 服务名只在共享网络内有效,不是全局 DNS;
- 容器重建后 IP 可能变化,应用不应硬编码容器 IP。
4. 宿主机防火墙和 Docker 规则
Docker 发布端口通常依赖 Linux 转发、NAT 及防火墙规则。若宿主机启用了 firewalld、ufw、nftables 自定义规则,或者云平台安全组拒绝流量,容器内服务可能正常而外部访问失败。
按层验证:
# 容器内是否监听
docker exec web ss -lnt
# 宿主机本地访问发布端口
curl -v http://127.0.0.1:8080/
# 查看路由和接口
ip addr
ip route
# 查看防火墙规则,命令依发行版而异
sudo nft list ruleset
sudo iptables -t nat -S
不要为了“快速验证”直接清空 iptables/nftables 规则。这样可能断开 SSH、暴露其他服务,或破坏系统防火墙管理工具的状态。应先保存规则并明确修改来源。
5. Overlay 的边界
Overlay 网络用于跨 Docker 主机连接容器,典型场景是 Swarm service。它需要节点间的控制和数据路径,并涉及 VXLAN、节点发现以及相应端口;具体端口和实现取决于部署方式与防火墙配置。
普通 docker compose up 在单机上通常使用 bridge 网络。Compose 文件中写出:
networks:
app:
driver: overlay
并不意味着单机 Compose 自动获得完整的多主机编排能力。排查 Overlay 时必须同时检查:
- 节点是否加入同一 Swarm;
- 节点间控制平面和数据平面是否连通;
- service/task 是否实际连接了该 Overlay 网络;
- 加密、MTU 和底层网络是否匹配。
因此,不能用“单机容器之间能通信”推断 Overlay 跨节点一定正常。
五、磁盘故障:区分镜像层、容器可写层、卷、日志和 BuildKit 缓存
1. Docker 使用的磁盘不是一个单一空间
Docker 相关空间至少包括:
镜像只读层
容器可写层
命名卷
bind mount 对应的宿主机目录
容器日志
BuildKit 构建缓存
镜像和容器元数据
查看概览:
docker system df
docker system df -v
df -h
df -i
df -h 查看文件系统块空间;df -i 查看 inode。磁盘还有空间但 inode 耗尽时,创建文件同样会失败,例如:
no space left on device
docker system df 主要展示 Docker 认为可回收或正在使用的对象,不一定覆盖 bind mount 目录中应用自己写入的所有文件。
2. 容器写入层的增长机制
镜像层是只读的。容器运行时对已有文件的修改通常触发 copy-up:文件从只读层复制到容器可写层,再在可写层中修改。删除镜像中的大文件也不会让镜像层缩小,因为只是在上层创建删除标记。
例如:
FROM alpine
RUN dd if=/dev/zero of=/bigfile bs=1M count=200
RUN rm /bigfile
最终镜像仍可能包含接近 200 MB 的历史层,因为 rm 只发生在后续层。正确的构建方式是将生成和删除放在同一层:
FROM alpine
RUN dd if=/dev/zero of=/tmp/bigfile bs=1M count=200 \
&& rm /tmp/bigfile
实际生产中不应为了示例生成无意义的大文件,但这个差异说明了镜像层的不可变性质。
查看容器写入量:
docker ps -s
docker inspect CONTAINER \
--format 'UpperDir={{.GraphDriver.Data.UpperDir}}'
GraphDriver.Data.UpperDir 属于实现细节,存储驱动不同、rootless 模式或未来版本下可能不同;不要把它作为应用写入路径。更可靠的是让应用数据写入卷或 bind mount,再从宿主机直接检查该目录。
3. 卷和 bind mount 的重要区别
docker volume ls
docker volume inspect VOLUME
findmnt
命名卷由 Docker 管理,默认位于 Docker 数据根目录下的某个卷路径;bind mount 使用宿主机指定目录。两者都绕过容器可写层,但生命周期和权限语义不同:
- 删除容器通常不会自动删除命名卷;
- bind mount 中的数据不由 Docker 的镜像/容器清理命令管理;
- 挂载点下镜像原有文件会被遮蔽,而不是合并;
- 宿主机 UID/GID、SELinux 标签、只读属性可能导致应用无法写入。
在 Linux 上,若容器以非 root 用户运行,应验证宿主机目录的数值 UID/GID 和权限,而不是只看用户名:
stat -c '%u:%g %a %n' /srv/app-data
docker exec CONTAINER id
4. 日志可能是磁盘增长的主要来源
查看当前容器的日志配置:
docker inspect CONTAINER \
--format '{{json .HostConfig.LogConfig}}'
默认日志驱动和配置由 Docker 安装及 Daemon 配置决定,不能假设所有环境都是 json-file。若使用 json-file,日志通常位于 Docker 数据根目录下的容器目录,但直接删除正在使用的日志文件可能造成日志驱动状态异常,不应把 rm 当作清理方案。
较安全的方向是:
- 为适用的 logging driver 配置轮转或远端收集;
- 限制应用日志级别和单条消息大小;
- 对配置变更后的新容器进行验证;
- 通过
docker inspect确认实际 driver,而不是只检查配置文件。
使用 docker system prune、docker image prune 或 docker builder prune 前要明确回收对象。尤其是:
docker system prune --volumes
可能删除未被容器引用的卷,卷中的数据库数据可能因此丢失。生产清理应先列出候选对象、确认备份和引用关系,再执行回收。
5. “Docker 占满磁盘”也可能是删除但仍被进程打开的文件
Linux 文件只有在目录项删除且没有进程持有打开描述符时,空间才真正释放。可检查:
sudo lsof +L1
如果应用删除了旧日志但仍保持文件描述符,du 可能看不到文件,df 却显示空间未释放。此时通常需要让应用重新打开日志文件或重启相关进程,而不是继续删除目录。
六、OOM:退出码、cgroup 事件和内核证据必须同时成立
1. OOM 的定义和故障路径
OOM(Out Of Memory)表示内存分配需求无法满足。Linux 上至少有两种相关情形:
- 容器 cgroup 达到内存限制:内核在该 cgroup 内选择进程杀死;
- 宿主机整体内存不足:全局 OOM killer 选择进程,可能是 Docker 容器,也可能是宿主机上的其他进程。
因此,Exited (137) 只能说明进程通常收到了 SIGKILL:
137 = 128 + 9
其中 9 是 SIGKILL。它不能单独证明发生了 OOM。
检查容器状态:
docker inspect CONTAINER \
--format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}}'
检查内核证据:
dmesg -T | grep -i -E 'oom|killed process|memory cgroup'
journalctl -k --since "10 minutes ago" | grep -i -E 'oom|killed process|memory cgroup'
现代 Linux 许多发行版使用 cgroup v2。可以先查看 Docker 使用的 cgroup 信息:
docker info | grep -i -E 'Cgroup|Memory'
docker stats --no-stream CONTAINER
docker stats 展示的是运行时观测值;它不是完整的内核 OOM 证据。cgroup v2 中还可以在对应 cgroup 目录查看:
memory.current
memory.max
memory.events
memory.stat
路径依发行版和 Docker 模式而异,不应硬编码 /sys/fs/cgroup/docker/...。通过 docker inspect、systemd slice 或 /proc/<pid>/cgroup 定位实际 cgroup 更可靠。
2. 内存限制的因果关系
如果容器配置:
docker run --memory=512m --memory-swap=512m IMAGE
含义通常是容器内存上限约为 512 MiB,并且内存加 swap 的总上限也为 512 MiB,等价于没有额外 swap 空间可用。精确行为受内核 cgroup、swap 是否启用和 Docker 版本影响。
Compose 中常见配置是:
services:
api:
image: example/api
mem_limit: 512m
Compose 规范中的资源字段与 Swarm 部署字段并不完全等价。deploy.resources.limits 在不同 Compose 实现和部署目标上的处理可能不同;使用前应通过 docker compose config 和实际 docker inspect 验证最终容器配置。
docker stats 中内存使用还可能包含 page cache。对于排查“应用堆内存不高但容器达到限制”的现象,应结合 memory.stat、应用运行时指标和内核事件,而不是只看一个百分比。
3. 诊断 OOM 的完整算例
假设容器状态为:
ExitCode=137
OOMKilled=true
并且 memory.events 显示:
oom 3
oom_kill 1
可以按以下逻辑推导:
137表明主进程被强制杀死;OOMKilled=true表明 Docker 记录到容器级 OOM;oom_kill=1表明 cgroup 内确实发生过 OOM kill;- 若
memory.max为536870912,则限制约为 512 MiB; - 应用在峰值期间超过该上限,内核选择进程终止;
- 若容器配置了自动重启,Docker 又启动新实例,于是表现为反复
Restarting。
这个推导不能反过来简化为“所有 137 都是 OOM”。例如管理员执行:
docker kill CONTAINER
也可能产生 137,但不会得到同样的 OOM 证据。
4. 恢复策略必须针对原因
短期恢复可以增加容器内存限制或释放宿主机资源,但长期修复要区分:
- 应用堆泄漏:修复对象生命周期、缓存策略或连接池;
- 峰值过高:调整并发、批量大小、启动时预加载;
- 子进程未回收:检查 PID 1 和 worker 管理;
- 容器限制过低:根据工作集、峰值和宿主机容量重新设置;
- 宿主机整体内存不足:检查所有进程,而不仅是 Docker。
不要直接禁用 OOM 行为或无限提高限制。没有足够物理内存和 swap 时,单个容器的限制调整可能只是把故障转移为宿主机级 OOM。
七、构建失败:BuildKit 的输入、执行器、缓存和输出要分别检查
1. 构建不是“把 Dockerfile 发给 Daemon”这么简单
现代 Docker 默认使用 BuildKit。一次构建包含:
客户端确定构建上下文
↓
读取 Dockerfile 和 .dockerignore
↓
前端解析 Dockerfile 为构建图
↓
BuildKit 并行执行步骤
↓
拉取基础镜像、执行 RUN、导出缓存
↓
导出镜像或推送 registry
构建上下文是发送给构建器的文件集合,不是 Dockerfile 所在目录的抽象引用。以下命令将当前目录作为上下文:
docker build -t demo:dev .
如果 COPY app /app 报错,首先检查 app 是否在上下文内,以及是否被 .dockerignore 排除:
find . -maxdepth 2 -type f | sort
cat .dockerignore
docker build --progress=plain -t demo:dev .
.dockerignore 排除文件后,即使宿主机目录中确实存在该文件,Dockerfile 也不能 COPY 它。这是输入边界导致的错误,不是容器运行时挂载错误。
2. 用 --progress=plain 保留可检索证据
BuildKit 默认输出可能是动态 TTY。排查时使用:
docker build --progress=plain -t demo:debug .
重点观察:
- 基础镜像解析和拉取是否成功;
- 失败发生在
RUN、COPY、导出镜像还是推送阶段; - 是否因为代理、DNS、证书或 registry 认证失败;
- 是否因为上下文过大、磁盘不足或缓存导出失败。
如果怀疑缓存掩盖了问题:
docker build --no-cache --progress=plain -t demo:debug .
--no-cache 会让构建重新执行,增加网络、CPU、磁盘和时间消耗;它适合验证“缓存是否导致旧结果”,不应作为所有构建失败的固定答案。
3. Dockerfile 层和缓存的真实边界
例如:
FROM alpine:3.20
WORKDIR /app
COPY package-list.txt .
RUN apk add --no-cache $(cat package-list.txt)
COPY . .
RUN ./build.sh
把变化频繁的源码放在后面,有机会复用依赖安装层。但缓存是否命中取决于指令、输入文件、基础镜像和构建参数等内容;不能假设“上一层成功过就永远复用”。
反例:
COPY . .
RUN apk add --no-cache ...
任何源码文件变化都可能使后续依赖安装层失效。这个问题首先是缓存设计问题,不一定是 BuildKit 损坏。
BuildKit 可能并行执行互不依赖的构建阶段,因此日志显示顺序不总是等于 Dockerfile 从上到下的单线程执行顺序。多阶段构建可以把编译环境和运行环境分开:
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server
FROM alpine:3.20
COPY --from=build /out/server /usr/local/bin/server
ENTRYPOINT ["/usr/local/bin/server"]
这个示例依赖项目存在 ./cmd/server,且生成的二进制适合目标运行环境。CGO_ENABLED=0 只是示例选择;如果程序依赖 glibc、动态库或 CGO,直接复制到 Alpine 可能运行失败。
4. 架构、网络和秘密信息
构建时出现:
exec format error
可能不是运行阶段问题,也可能发生在 RUN 执行中:构建器执行了不适合当前构建平台的二进制。先查看平台:
docker buildx ls
docker version --format '{{.Server.Os}}/{{.Server.Arch}}'
跨平台构建需要确认 builder、目标平台、模拟器或原生节点是否支持。即使镜像成功构建,运行时仍可能因架构不匹配失败。
构建访问私有 registry、软件源或 Git 仓库时,失败路径可能是:
BuildKit → DNS → 代理/防火墙 → registry 或软件源
这与“容器运行网络正常”不是同一条路径。构建器所在节点、代理配置和证书信任链都要单独检查。
不要把密码、云凭据写入:
ARG TOKEN=...
ENV TOKEN=...
RUN echo "$TOKEN" ...
因为它们可能进入构建历史、缓存或镜像元数据。应使用 BuildKit 支持的 secret 或 SSH mount,并验证最终镜像中没有秘密文件。秘密 mount 的具体命令和语法依赖 BuildKit/Buildx 版本,使用前应以当前 Docker 文档和 docker buildx build --help 为准。
5. 构建缓存占满磁盘
查看构建缓存:
docker buildx du
清理未使用 BuildKit 缓存:
docker buildx prune
命令可能提示删除缓存对象。生产环境中应先确认没有其他项目依赖这些缓存;清理后下一次构建会重新下载依赖并增加构建时间。镜像空间和 BuildKit 缓存是相关但不同的回收对象,不能只执行 docker image prune 就认为构建缓存已清空。
八、Daemon 故障:先判断是服务、配置、存储还是内核依赖
1. systemd 主机上的第一组命令
sudo systemctl status docker.service --no-pager
sudo journalctl -u docker.service -b --no-pager -n 200
sudo systemctl cat docker.service
如果 Daemon 反复启动失败,重点看第一次失败的根因,而不是最后一条 systemd 的“进程退出”摘要。常见根因包括:
/etc/docker/daemon.jsonJSON 格式错误或配置项不兼容;- Docker 数据根目录所在文件系统不可写或已满;
- overlay2、containerd、runc 或内核能力不可用;
- cgroup 配置不匹配;
- iptables/nftables 初始化失败;
- 监听 socket、TLS 证书或端口已被占用;
- 代理、registry 或证书配置导致依赖初始化失败。
检查配置文件和磁盘:
sudo sed -n '1,240p' /etc/docker/daemon.json
sudo df -hT /var/lib/docker /var/lib/containerd
sudo df -i /var/lib/docker /var/lib/containerd
daemon.json 必须是合法 JSON。例如,下面是合法形式:
{
"log-driver": "local",
"data-root": "/var/lib/docker"
}
但配置项是否被当前 Engine 支持,仍需结合版本验证。不要把网上其他版本的配置直接复制到生产主机。
2. 客户端错误和 Daemon 错误的区分
下面两类错误根因不同:
permission denied while trying to connect to the Docker daemon socket
通常是客户端访问 socket 的权限问题。
failed to start daemon: error initializing graphdriver
通常是 Daemon 初始化存储驱动失败。
而:
pull access denied for IMAGE
通常表示 registry 认证、仓库权限或镜像名称问题,不等于本地 Daemon 停止。
可用:
docker version
docker info
确认 Server 是否可用,再处理镜像、网络或权限问题。
3. dockerd 的数据根目录不是普通缓存目录
默认数据目录常见为 /var/lib/docker,但可以通过 data-root 改变;rootless Docker 的路径也不同。数据目录包含镜像层、容器元数据、卷和其他状态。不要在 Daemon 运行时直接删除其中的目录,例如:
sudo rm -rf /var/lib/docker/*
这不是清理,而是破坏 Docker 状态,可能造成所有镜像、容器和卷不可恢复。磁盘修复应优先使用 Docker 自身的对象清理、日志策略和缓存回收;若数据目录损坏,应先停止服务、制作文件系统级备份或快照,再按存储驱动和灾备方案恢复。
4. 重启、reload 和恢复验证
修改 Daemon 配置后,在 systemd 主机上通常需要:
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl status docker --no-pager
docker info
daemon-reload 只重新读取 systemd unit,不会验证 daemon.json;restart 才会让 Docker 重新初始化。重启 Daemon 会影响容器运行、网络和 API 请求,是否自动恢复容器取决于 restart policy 和具体故障阶段。
验证恢复不能只看 systemd:
docker info
docker run --rm hello-world
docker network create diag-net
docker run --rm --network diag-net alpine getent hosts alpine
docker network rm diag-net
最后两个命令依赖镜像内存在 getent,且需要能够拉取 Alpine。若生产环境禁止外网,应换成已批准的本地诊断镜像。验证应覆盖 API、镜像创建、网络创建和容器启动,而不仅是“服务状态 active”。
九、一个可重复的现场排障流程
第一步:记录环境和时间范围
date -Is
hostname
docker version
docker info
docker context show
uname -a
记录 Engine 版本、内核、架构、存储驱动、Cgroup 版本、日志驱动和 Docker 根目录。版本差异会影响 BuildKit、Compose 字段、iptables/nftables 和 cgroup 行为。
第二步:判断故障层级
docker ps -a
docker events --since 30m
sudo journalctl -u docker.service --since "30 minutes ago"
问题是:
- 所有容器都无法启动:优先检查 Daemon、存储、cgroup 和网络初始化;
- 单个容器失败:检查镜像、入口、挂载和应用;
- 容器都在运行但互相不可达:检查网络和 DNS;
- 构建失败但已有容器正常:检查 BuildKit、上下文、registry 和构建磁盘;
- 宿主机卡顿且多个服务被杀:检查系统级内存和磁盘,而不是只看单个容器。
第三步:对单个容器建立证据链
docker inspect CONTAINER > inspect.json
docker logs --timestamps --tail=500 CONTAINER
docker stats --no-stream CONTAINER
docker port CONTAINER
docker network inspect NETWORK
把 inspect.json 保存下来,避免容器被重建后状态丢失。特别记录 ExitCode、OOMKilled、挂载、环境变量、网络 IP、restart policy 和日志配置。
第四步:用最小复现缩小范围
- 去掉 Compose,仅运行镜像;
- 去掉原始入口,进入 shell;
- 去掉外部网络,仅测试本地进程;
- 去掉持久化挂载,确认是否是权限或数据损坏;
- 使用
--progress=plain和--no-cache区分 BuildKit 缓存问题; - 使用临时网络诊断容器,但保持相同 network namespace 或目标网络。
每减少一个变量,都要记录“哪一步恢复、哪一步再次失败”。否则重试本身不会产生因果证据。
十、常见误判与正确解释
“容器是 Up,所以服务正常”
Up 只说明 PID 1 仍存在。应用可能已经死锁、无法接受连接或健康检查失败。若配置了健康检查,应查看:
docker inspect --format '{{json .State.Health}}' CONTAINER
健康检查失败不会自动等价于容器退出;是否重启取决于编排和外部控制器。
“能 ping,所以端口没问题”
ICMP、DNS、TCP 和应用协议是不同层次。正确顺序应是:
名称解析 → 路由 → TCP 建连 → TLS/HTTP/数据库协议 → 业务响应
每层成功只证明该层,不证明后续层。
“磁盘满了,执行 system prune 就行”
system prune 只能清理符合条件的 Docker 对象,不能清理 bind mount 中的应用数据、被进程打开的已删除文件,也不能代替日志轮转。带 --volumes 的清理还可能删除数据卷。
“退出码 137 就一定是 OOM”
137 只表示 SIGKILL 的常见编码。必须同时检查 OOMKilled、内核日志和 cgroup 事件。
“构建用了缓存,所以结果一定正确”
缓存保证的是在满足缓存键条件时复用已有结果,不保证外部网络内容、时间、远程仓库分支或未固定依赖永远不变。需要可重复构建时,应固定基础镜像摘要、依赖版本和构建输入,并明确缓存导入导出策略。
结语:故障排查的核心是还原状态转换
Docker 故障不是一个统一的“容器问题”。应沿着真实状态转换还原:
CLI 是否连到正确 Daemon
→ Daemon 是否能读取配置和存储
→ 镜像和构建输入是否有效
→ rootfs、挂载、网络和 cgroup 是否创建成功
→ PID 1 是否启动并保持运行
→ DNS、路由、端口和应用协议是否逐层通过
→ 磁盘、内存和内核是否在运行期间触发限制
每个结论都应至少由一个可观察证据支持:容器状态、结构化 inspect、容器日志、Docker 事件、Daemon journal、内核日志、cgroup 文件、网络连接或文件系统统计。这样才能把“重启后暂时恢复”进一步还原为可验证的故障原因,并选择不会破坏数据和宿主机安全边界的恢复动作。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 数据备份与主机迁移:卷、数据库、一致性和恢复演练
- 下一篇:Docker Engine API 与自动化:SDK、事件、权限和幂等操作
- 延伸:Docker 网络完整指南:Bridge、端口映射、DNS、Overlay 和排障
- 延伸:Docker 日志与监控:Logging Driver、Metrics、事件和容量告警
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论