背景
Docker 已经是现代运维和开发的基础设施,但在生产环境中使用 Docker,由于环境的复杂性和容器的特殊性,很多在物理机或虚拟机上不会出的问题在容器环境下会集中爆发。本篇从实际生产故障中提炼出 10 个最容易踩的坑,每个坑都给出真实的现象描述、原理说明、排查命令和修复方案。
这些坑覆盖了镜像管理、容器生命周期、网络配置、存储管理、安全加固、监控告警等 Docker 使用中最常见的领域。
坑一:Docker 存储空间耗尽(Disk Full)
现象
容器无法启动,日志报错 no space left on device
docker ps 报错 Cannot connect to the Docker daemon
df -h 显示 /var/lib/docker 所在磁盘使用率 100%
写入文件时报 "No space left on device"
原理
Docker 的存储驱动(overlay2、devicemapper、btrfs、zfs)默认把镜像层、容器层、日志、构建缓存都放在 /var/lib/docker 下。如果这个分区没有独立mount,或者根分区空间有限,容器日志、镜像、build 缓存很容易把它撑满。
排查命令
# 查看 Docker 数据目录所在磁盘的使用情况 df -h /var/lib/docker # 查看 Docker 占用的磁盘空间分布 docker system df # 详细看各部分占用 docker system df -v # 查看容器日志大小 ls -lh /var/lib/docker/containers/*/*-json.log # 查看 overlay2 层的实际占用 du -sh /var/lib/docker/overlay2/*
修复方案
# 1. 清理悬空镜像(没有 tag 的镜像)
docker image prune -a
# 2. 清理构建缓存
docker builder prune -a
# 3. 清理所有未使用的资源(镜像、容器、网络、构建缓存)
docker system prune -a --volumes
# 4. 限制容器日志大小(需要修改 docker daemon 配置或 docker-compose)
# 方法一:全局限制(修改 /etc/docker/daemon.json)
# {
# "log-driver": "json-file",
# "log-opts": {
# "max-size": "100m",
# "max-file": "3"
# }
# }
# 5. 手动清理容器日志(临时方案,不推荐但紧急时可用)
# 先停止容器,截断日志文件,再启动
> /var/lib/docker/containers//*-json.log
预防措施
把 /var/lib/docker 放在独立分区或 LVM 逻辑卷
配置容器日志轮转(max-size + max-file)
定期清理镜像和构建缓存
监控磁盘使用率,超过 80% 告警
坑二:容器内时间与宿主机时间不一致
现象
容器内 date 命令输出和宿主机差 8 小时
程序日志时间戳和实际时间不符
数据库写入的时间差了 8 小时
证书有效期计算错误
原理
容器默认使用宿主机的 kernel,没有自己的时区设置。如果宿主机是 CST(UTC+8),但容器没有正确挂载时区文件,就会使用 UTC 时间。
排查命令
# 查看宿主机时间 date # 查看容器内时间 docker execdate # 检查容器是否挂载了时区文件 docker inspect | grep -A 20 "Mounts"
修复方案
方案一:运行时挂载时区文件
docker run -v /etc/timezone:/etc/timezone:ro -v /etc/localtime:/etc/localtime:ro nginx
方案二:设置环境变量(部分基础镜像支持)
docker run -e TZ=Asia/Shanghai nginx
方案三:docker-compose 方式
services: app: image: my-app:latest environment: TZ: "Asia/Shanghai" volumes: - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro
方案四:Dockerfile 中设置时区(基础镜像构建时)
FROM ubuntu:20.04 RUN apt-get update && apt-get install -y tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone
坑三:容器内无法解析 DNS(内网域名)
现象
宿主机能 ping 通 redis-master,容器内 ping 不通
curl http://nginx 在宿主机正常,容器内失败
容器能解析公网 DNS(curl baidu.com 正常),但无法解析内网域名
跨容器通信时报 could not resolve host
原理
Docker 默认使用内置的 DNS 服务(地址 127.0.0.11),这个 DNS 服务知道容器通过 --link 或 docker network 建立的内部域名,但不知道宿主机网络中的自定义 DNS 记录(如公司内网 DNS 服务器上的 redis-master.internal)。
排查命令
# 查看容器 DNS 配置 docker execcat /etc/resolv.conf # 查看容器网络模式 docker inspect | grep -A 10 "NetworkSettings" # 从容器内测试 DNS docker exec nslookup nginx docker exec dig nginx # 查看宿主机 DNS 配置 cat /etc/resolv.conf
修复方案
方案一:使用 --dns 指定 DNS 服务器
docker run --dns 192.168.1.53 nginx
方案二:docker-compose 配置 DNS
services: app: image:my-app:latest dns: -192.168.1.53 -8.8.8.8 networks: -my-net networks: my-net: driver:bridge ipam: config: -subnet:172.20.0.0/16
方案三:配置 daemon.json 全局 DNS
{
"dns": ["192.168.1.53", "8.8.8.8"]
}
注意:修改 daemon.json 需要 systemctl restart docker 才能生效,会影响所有容器。
坑四:容器进程被OOMKilled
现象
docker ps 显示容器退出了
docker logs
docker inspect
宿主机 dmesg | grep -i oom 或 journalctl | grep -i oom 有记录
原理
容器的内存限制由 Linux cgroup 控制。当容器内的进程试图申请超过 limit 的内存时,Linux 会触发 OOM Killer 选择容器内的一个进程杀掉。如果进程没有处理 SIGKILL 信号,容器会直接退出。
排查命令
# 检查容器退出状态 docker inspect| grep -E "OOMKilled|ExitCode|State" # 查看容器内存使用峰值 docker stats --no-stream # 查看容器内存限制 docker inspect | grep -A 5 "Memory" # 在宿主机上查看 cgroup 内存统计 cat /sys/fs/cgroup/memory/docker/ /memory.usage_in_bytes cat /sys/fs/cgroup/memory/docker/ /memory.limit_in_bytes # 查看宿主机 OOM 日志 dmesg | grep -i "out of memory" dmesg | grep -i "killed process" journalctl | grep -i oom | tail -20
修复方案
# 1. 紧急:提高内存限制重新启动容器 docker run --memory=1g my-app:latest # 2. docker-compose 方式 # docker-compose.yml services: app: image: my-app:latest mem_limit: 1g mem_reservation: 512m # 3. 如果是 Java 应用,确保 JVM 堆内存 <= 容器内存 limit # JVM 堆外内存(native、direct buffer、mmap)也需要考虑 # 建议 JVM -Xmx 设置为容器 limit 的 75-80% docker run -e JAVA_OPTS="-Xmx768m" --memory=1g my-java-app # 4. 如果是正常业务增长,考虑水平扩展(多容器实例)
预防措施
设置合理的内存 limit,不要设置得过高或过低
Java/Node.js 等有自己内存管理的应用,要明确设置堆内存
配置监控告警:内存使用率超过 80% limit 时告警
在宿主机部署 OOM 告警脚本
坑五:容器无法访问外网,但宿主机正常
现象
ping baidu.com 在宿主机正常,容器内不通
curl https://google.com 在宿主机正常,容器内超时
容器间通信正常(同一个 bridge 网络内)
从容器内访问宿主机 IP 正常,但访问其他 IP 不通
原理
这种情况通常是 Docker 的包过滤(iptables/IPVS)或网络 MTU 问题导致的。常见原因:
MTU 不匹配:容器默认使用 docker0 网桥的 MTU(默认 1500),如果宿主机网卡的 MTU 是 9000(jumbo frame),路径 MTU 发现可能失败
iptables 规则被意外修改:Docker 会自动添加 iptables NAT 规则,如果被清理,容器无法通过 NAT 访问外网
宿主机开启了数据包转发但 Docker 没正确配置
代理设置:宿主机走了代理但容器没有
排查命令
# 检查容器网络连通性(按顺序测试) docker execping 8.8.8.8 # 测试 IP 层连通性 docker exec ping baidu.com # 测试 DNS 解析 docker exec curl -v https://google.com # 测试应用层 # 检查宿主机 iptables NAT 规则 iptables -t nat -L -n | grep DOCKER # 检查 Docker 网桥配置 ip addr show docker0 ip route show # 检查 MTU ip link show eth0 docker network inspect bridge | grep -i mtu # 抓包分析 tcpdump -i docker0 -n host 8.8.8.8
修复方案
MTU 问题:
# 方法一:启动容器时指定 MTU
docker run --network=host --mtu=9000 my-app
# 方法二:修改 daemon.json 全局配置
# /etc/docker/daemon.json
{
"mtu": 9000
}
iptables 规则被清理:
# 重置 Docker 的 iptables 规则 iptables -t nat -F iptables -t filter -F systemctl restart docker # Docker 重启后会自动添加正确的 iptables 规则
代理问题:
# 检查宿主机代理 echo $http_proxy echo $https_proxy # 容器内设置代理(如果宿主机走了代理) docker run -e HTTP_PROXY=http://host.docker.internal:7890 my-app
坑六:删除容器后数据丢失
现象
重新部署容器后,之前写入的数据找不到了
数据库容器重启后变成空库
配置文件修改后,重启容器又恢复成默认配置
原理
默认情况下,容器内的文件系统是「写时复制」(copy-on-write)的,容器删除后,这一层也跟着没了。容器内的数据默认不会持久化到宿主机。除非使用:
数据卷(Volume):存储在 /var/lib/docker/volumes/
绑定挂载(Bind Mount):直接映射宿主机的目录
tmpfs mount:存在内存中
排查命令
# 查看容器的挂载信息 docker inspect| grep -A 20 "Mounts" # 查看数据卷列表 docker volume ls # 查看数据卷详情 docker volume inspect # 检查宿主机上的数据卷路径 ls -la /var/lib/docker/volumes/ /_data
修复方案
使用命名数据卷持久化 MySQL 数据:
# docker-compose.yml services: mysql: image:mysql:8.0 environment: MYSQL_ROOT_PASSWORD:"password" volumes: -mysql_data:/var/lib/mysql ports: -"3306:3306" volumes: mysql_data: driver:local
使用绑定挂载持久化配置文件:
services: nginx: image: nginx:1.24 volumes: - /data/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - /data/nginx/logs:/var/log/nginx ports: - "80:80"
不要用匿名存储:
# 错误:匿名 volume,重启后数据可能丢失 volumes: - /var/lib/mysql # 匿名 volume,宿主机路径不明确 # 正确:命名 volume volumes: - mysql_data:/var/lib/mysql
坑七:Docker 镜像标签混乱(latest 陷阱)
现象
docker run my-app:latest 拉取了新版本,但业务逻辑没变
docker build -t my-app:1.0 . 打了 tag 但 docker images 显示
部署后发现版本不对,查不到是哪个镜像
原理
latest 只是一个普通的 tag,不是自动指向最新版本的特殊标签。如果你 docker build 时不指定 tag,默认就是 latest。很多人误以为 latest 总是指向最新版本,但实际上 latest 的指向完全取决于最后一次 docker build -t my-app:latest 或 docker tag 操作。
另外,本地 latest 和远程仓库的 latest 可能不是同一个版本。
排查命令
# 查看镜像的所有 tag docker images my-app # 查看镜像的创建时间 docker inspect my-app:latest | grep Created # 查看镜像的完整 ID docker images --no-trunc my-app # 对比本地 latest 和远程 latest 是否相同 docker pull my-app:latest docker images my-app:latest
修复方案
始终使用具体版本标签,不要用 latest:
# Dockerfile FROM nginx:1.24.0-alpine # 不用 latest,用精确版本
# 构建时指定精确版本 docker build -t my-app:1.2.3 . docker build -t my-app:release-20240115 . # 打 tag 说明提交 hash docker build -t my-app:v1.2.3-$(git rev-parse --short HEAD) .
GitOps 工作流:
# Jenkinsfile / GitLab CI / GitHub Actions
stages:
-build
-push
-deploy
build:
stage:build
script:
-IMAGE_TAG=${CI_COMMIT_SHORT_SHA}-${CI_BUILD_ID}
-dockerbuild-tregistry.example.com/my-app:${IMAGE_TAG}.
-dockerpushregistry.example.com/my-app:${IMAGE_TAG}
-echo${IMAGE_TAG}>image_tag.txt
deploy:
stage:deploy
script:
-IMAGE_TAG=$(catimage_tag.txt)
-kubectlsetimagedeployment/my-appapp=registry.example.com/my-app:${IMAGE_TAG}
坑八:容器进程PID 1 和信号处理问题
现象
docker stop
容器收到 SIGTERM 后没有优雅退出
docker kill
日志显示 main process exited, code 0 但子进程变成了僵尸进程
原理
容器内的 PID 1 进程对信号处理有特殊要求。默认情况下,Docker 将信号转发给 PID 1 进程,但:
如果 PID 1 进程是 shell 脚本(如 CMD ["/bin/sh", "-c", "java -jar app.jar"]),shell 本身不转发信号,Java 进程收不到 SIGTERM
如果 PID 1 进程没有正确处理 SIGTERM,容器会一直等直到 timeout(默认 10 秒),然后被 SIGKILL
某些基础镜像的 PID 1 不是应用本身,而是 tini 或 systemd
排查命令
# 查看容器内的进程树 docker execps aux # 查看 PID 1 的进程是什么 docker exec cat /proc/1/cmdline | tr '�' ' ' docker exec ps -p 1 # 测试发送 SIGTERM 后的停止时间 time docker stop
修复方案
方案一:使用 exec 形式的 CMD(让信号直接发给应用)
# 错误:shell 形式,shell 作为 PID 1,不转发信号 CMD java -jar app.jar CMD ["/bin/sh", "-c", "java -jar app.jar"] # 正确:exec 形式,直接运行应用,应用作为 PID 1 CMD ["java", "-jar", "app.jar"] # 如果需要运行脚本,用 exec 把信号转发给子进程 ENTRYPOINT ["/entrypoint.sh"] # entrypoint.sh 内容: # #!/bin/bash # trap 'kill -TERM $PID' TERM INT # java -jar app.jar & # PID=$! # wait $PID
方案二:使用 init 进程(Docker 20.10+ 内置 tini)
docker run --init my-app:latest
# Dockerfile FROM alpine RUN apk add --no-cache tini ENTRYPOINT ["/sbin/tini", "--"] CMD ["java", "-jar", "app.jar"]
方案三:设置 stopTimeout(适用于 Docker Swarm / Compose)
services: app: image: my-app:latest stop_grace_period: 30s # 给容器 30 秒的优雅停止时间 stop_signal: SIGTERM
坑九:生产环境忘记设资源限制导致雪崩
现象
单台宿主机上跑了太多容器,内存被打爆
一个容器内的 Java 应用内存泄漏把整个宿主机的容器都拖垮
容器被 OOMKill 后重启,但重启后又 OOM,形成死亡循环
宿主机 Load 飙到 100+,所有容器响应极慢
原理
没有资源限制的容器理论上可以使用宿主机全部资源。当一个容器出问题(如内存泄漏),它会吸干宿主机的内存,导致:
其他容器因内存不足被 OOMKill
Docker daemon 本身也可能受影响
宿主机 kernel 进入 OOM,响应变慢
这就是"雪崩"——一个点的问题扩散到整个系统。
排查命令
# 查看所有容器内存使用
docker stats --no-stream
# 查看运行中的容器及其资源限制
docker ps --format "table {{.Names}} {{.Image}} {{.Status}} {{.Ports}}"
# 查看设置了内存限制的容器
docker ps --format "{{.Names}}" | whileread name; do
limit=$(docker inspect $name --format '{{.HostConfig.Memory}}')
echo"$name: $limit"
done
# 查看宿主机的整体资源使用
top
free -h
df -h
修复方案
Always set resource limits in production:
services: app: image:my-app:latest deploy: resources: limits: memory:512M cpus:'0.5' reservations: memory:256M cpus:'0.25' restart_policy: condition:on-failure max_attempts:3
# 命令行方式 docker run -d --memory=512m --memory-reservation=256m --cpus=0.5 --cpus-reservation=0.25 --restart=on-failure:3 my-app:latest
内存限制要设置,但不要设置过大:
# 假设应用正常需要 400M,设置 512M,留点余量 --memory=512m # 不要设太大(如 4G),否则调度器无法感知实际需求
坑十:Docker daemon 安全暴露(2375/2376端口)
现象
阿里云/腾讯云控制台告警:服务器开放了 Docker 2375 端口
curl http://server:2375/info 能获取 Docker daemon 完整信息
docker -H tcp://server:2375 ps 能在本地操作远程服务器上的容器
服务器被入侵,挖矿程序通过 Docker 逃逸到宿主机
原理
Docker daemon 默认不开放 TCP 端口。如果管理员为了"方便管理"把 Docker 端口暴露到公网(-H tcp://0.0.0.0:2375),任何能访问到这个端口的人都可以:
在宿主机上以 root 权限运行任意容器
读取宿主机的所有文件
通过容器逃逸获得宿主机 root 权限
2375 是未加密的 Docker API,2376 是 TLS 加密版本。但即使 2376 如果没有正确配置证书,也是危险的。
排查命令
# 检查 Docker daemon 监听端口 ps aux | grep dockerd | grep -v grep ss -tlnp | grep docker # 检查 docker daemon 启动参数 systemctl cat docker | grep ExecStart # 测试是否对外开放 curl http://localhost:2375/info 2>/dev/null && echo "2375 is open" curl https://localhost:2376/info 2>/dev/null && echo "2376 is open" # 从外部测试(如果有权限) nmap -p 2375,2376
修复方案
立即关闭暴露的 Docker API:
# 如果是通过 systemd 启动,修改 unit 文件 # /etc/systemd/system/docker.service.d/override.conf [Service] ExecStart= ExecStart=/usr/bin/dockerd # 不要加 -H tcp://0.0.0.0:2375 # 重载配置 systemctl daemon-reload systemctl restart docker # 确认端口已关闭 ss -tlnp | grep docker
如果确实需要远程 Docker API 管理,用 TLS:
# 生成 TLS 证书(参考 Docker 官方文档)
# 使用 docker-machine 或手动生成 CA + server cert + client cert
# 配置 Docker daemon(/etc/docker/daemon.json)
{
"tls": true,
"tlscert": "/etc/docker/tls/server-cert.pem",
"tlskey": "/etc/docker/tls/server-key.pem",
"tlscacert": "/etc/docker/tls/ca.pem",
"hosts": ["fd://", "tcp://127.0.0.1:2376"]
}
# 客户端连接时必须带证书
docker -H tcp://server:2376 --tlsverify
--tlscert=client-cert.pem
--tlskey=client-key.pem
--tlscacert=ca.pem ps
网络层限制:
# 防火墙限制 Docker API 端口只能从管理网段访问 iptables -A INPUT -p tcp --dport 2375 -s 192.168.1.0/24 -j ACCEPT iptables -A INPUT -p tcp --dport 2375 -j DROP
容器安全最佳实践:
永远不要把 Docker API 端口暴露到公网
使用 --read-only 模式运行容器,防止写入恶意文件
使用 --security-opt=no-new-privileges 防止特权升级
不要用 --privileged 运行容器
定期审查容器能力:docker inspect --format '{{.HostConfig.CapAdd}}'
额外补充:容易被忽视的 5 个坑
坑十一:容器时区问题(和坑二重申)
坑十二:忘记 --restart always 导致容器退出后无人管
# 正确:生产环境建议用 restart-policy docker run -d --restart=unless-stopped my-app:latest # restart 策略选项: # no: 不自动重启(默认) # on-failure: 非零退出码时重启 # on-failure 非零退出码时最多重启 3 次 # always: 始终重启,dockerd 重启后也会重启 # unless-stopped: 始终重启,但 dockerd 重启前手动停掉的不会自动重启
坑十三:数据卷权限问题
# 挂载宿主机的目录给 Nginx,Nginx 进程用 nginx 用户运行 # 但 /data/www 是 root 拥有的,nginx 无法读取 docker run -v /data/www:/usr/share/nginx/html nginx # 解决方案一:容器内用 root 运行(不推荐生产) # 解决方案二:修改宿主机目录权限 chmod -R 755 /data/www # 解决方案三:在 Dockerfile 里创建用户并设置正确权限
坑十四:跨容器网络通信(bridge vs host)
# 默认 bridge 网络:容器间需要通过 IP 或 --link 别名通信 # 但 --link 已废弃,推荐用 user-defined bridge network # 正确做法:创建自定义网络 docker network create my-net docker run --network=my-net --name redis redis:alpine docker run --network=my-net --name app my-app:latest # app 容器内可以直接 ping redis,因为它们在同一网络 # host 网络:容器共享宿主机的网络命名空间 # 端口直接暴露到宿主机,但失去了网络隔离 docker run --network=host my-app:latest # 如果两个容器都绑定 80,会冲突
坑十五:多阶段构建泄露敏感信息
# 错误:在构建阶段把敏感信息带进了镜像 FROM golang:1.21 AS builder COPY . /app RUN go build -o app . # 运行阶段也包含了源码和构建工具 FROM alpine COPY --from=builder /app /app COPY --from=builder /root/.npm /root/.npm # 暴露了 npm 凭证 # 正确:多阶段构建,只复制最终产物 FROM golang:1.21 AS builder COPY . /app RUN go build -ldflags="-w -s" -o app . # builder 阶段使用的 .npmrc、.cargo 等不会进入最终镜像 FROM alpine COPY --from=builder /app /app RUN chmod +x /app CMD ["/app"]
总结
这 10 + 5 个坑是 Docker 生产环境中最常见的故障来源,按优先级整理如下:
| 坑 | 严重程度 | 出现频率 |
|---|---|---|
| Disk Full(存储空间耗尽) | 高(服务中断) | 极高 |
| Docker API 2375 端口暴露 | 极高(安全) | 高 |
| 没有设置资源限制 | 高(雪崩风险) | 极高 |
| OOMKilled | 高(服务中断) | 高 |
| 进程信号处理不当 | 中(重启慢/无法重启) | 高 |
| 容器内时间不一致 | 中(业务日志错乱) | 极高 |
| DNS 无法解析内网域名 | 中(服务不可用) | 高 |
| 数据没有持久化 | 高(数据丢失) | 高 |
| latest 标签混乱 | 低到中(版本错乱) | 高 |
| 无法访问外网 | 中(部分功能失效) | 中 |
建议把这些坑的排查命令整理成一张"Docker 故障排查卡",关键时刻能省大量时间。
全部0条评论
快来发表一下你的评论吧 !