Docker 生产环境踩坑指南
Docker 已经是现代运维和开发的基础设施,但容器环境的特殊性让很多物理机/虚拟机上不会出的问题集中爆发。 本篇从实际生产故障中提炼出 10 个最容易踩的坑,每个坑都给出真实的现象描述、原理说明、排查命令和修复方案。
优先级总览
| 坑 | 问题 | 严重程度 | 出现频率 |
|---|---|---|---|
| ① | 存储空间耗尽(Disk Full) | 高(服务中断) | 极高 |
| ② | 容器时间不一致 | 中(日志错乱) | 极高 |
| ③ | 内网 DNS 无法解析 | 中(服务不可用) | 高 |
| ④ | OOMKilled | 高(服务中断) | 高 |
| ⑤ | 容器无法访问外网 | 中(部分功能失效) | 中 |
| ⑥ | 数据未持久化 | 高(数据丢失) | 高 |
| ⑦ | 镜像标签混乱(latest 陷阱) | 低-中(版本错乱) | 高 |
| ⑧ | PID 1 信号处理不当 | 中(重启慢/无法优雅停止) | 高 |
| ⑨ | 未设资源限制导致雪崩 | 高(雪崩风险) | 极高 |
| ⑩ | Docker API 2375/2376 端口暴露 | 极高(安全) | 高 |
| ⑪-⑮ | 5 个易被忽视的坑 | 中-高 | 中-高 |
一、存储与资源
坑一:Docker 存储空间耗尽(Disk Full)
现象: 容器无法启动,日志报错 no space left on device;docker ps 报错 Cannot connect to the Docker daemon。
原理: Docker 的存储驱动(overlay2、devicemapper 等)默认把镜像层、容器层、日志、构建缓存都放在 /var/lib/docker 下。如果这个分区没有独立 mount,根分区很容易被撑满。
排查命令:
# 查看 Docker 数据目录所在磁盘使用情况
df -h /var/lib/docker
# 查看 Docker 占用的磁盘空间分布
docker system df -v
# 查看容器日志大小
ls -lh /var/lib/docker/containers/*/*-json.log
# 查看 overlay2 层的实际占用
du -sh /var/lib/docker/overlay2/*
修复方案:
# 清理悬空镜像(无 tag 的镜像)
docker image prune -a
# 清理构建缓存
docker builder prune -a
# 清理所有未使用的资源
docker system prune -a --volumes
预防措施: 把 /var/lib/docker 放在独立分区或 LVM 逻辑卷;配置容器日志轮转(max-size + max-file);定期清理镜像和构建缓存;监控磁盘使用率超过 80% 告警。
坑四:容器进程被 OOMKilled
现象: 容器退出,docker inspect 显示 OOMKilled: true,宿主机 dmesg 或 journalctl 有 OOM 记录。
原理: 容器的内存限制由 Linux cgroup 控制。当容器内进程试图申请超过 limit 的内存时,触发 OOM Killer 杀掉进程。如果进程不处理 SIGKILL,容器直接退出。
排查命令:
# 检查容器退出状态
docker inspect <container-id> | grep -E "OOMKilled|ExitCode|State"
# 查看容器内存使用峰值
docker stats <container-id> --no-stream
# 查看宿主机 OOM 日志
dmesg | grep -i "out of memory"
journalctl | grep -i oom | tail -20
Java 应用注意事项:
# JVM 堆内存 + native/direct buffer/mmap 等堆外内存之和 ≤ 容器 memory limit
# 建议 JVM -Xmx 设置为容器 limit 的 75-80%
docker run -e JAVA_OPTS="-Xmx768m" --memory=1g my-java-app
K8s 环境下 OOMKilled 的详细机制参见 k8s-resource-limits-configuration(QoS / oom_score_adj / Burstable 陷阱)。本页聚焦 Docker 原生视角。
坑九:未设资源限制导致雪崩
现象: 单台宿主机上跑太多容器,一个容器内存泄漏把整机拖垮;其他容器因内存不足被 OOMKill;宿主机 Load 飙到 100+。
原理: 没有资源限制的容器理论上可以使用宿主机全部资源。一个点出问题会扩散到整个系统。
# 查看所有容器资源使用
docker stats --no-stream
# 检查是否有容器未设限制
docker ps --format "{{.Names}}" | while read name; do
limit=$(docker inspect $name --format '{{.HostConfig.Memory}}')
echo "$name: $limit"
done
修复方案: Always set resource limits in production(Docker Compose 或 Docker run 命令行)。
参考 k8s-resource-limits-configuration 了解 Linux cgroup 层面资源限制原理,以及 Docker 与 K8s 两种限制方式的差异。
二、网络问题
坑三:内网 DNS 无法解析
现象: 宿主机能 ping 通内网域名(如 redis-master.internal),容器内 ping 不通;容器能解析公网 DNS 但无法解析内网域名。
原理: Docker 默认使用内置 DNS 服务(127.0.0.11),它知道容器内部域名但不知道宿主机网络中的自定义 DNS 记录。
修复方案:
# 方式一:运行时指定 DNS
docker run --dns 192.168.1.53 nginx
# 方式二:daemon.json 全局配置
# /etc/docker/daemon.json
{"dns": ["192.168.1.53", "8.8.8.8"]}
注意: 修改 daemon.json 需要 systemctl restart docker 才能生效,会影响所有容器。
坑五:容器无法访问外网
现象: ping baidu.com 在宿主机正常,容器内不通;容器间通信正常但访问外网超时。
常见原因:
- MTU 不匹配:容器默认 docker0 网桥 MTU 1500,宿主机网卡 MTU 9000 时路径 MTU 发现可能失败
- iptables 规则被意外修改:Docker 自动添加的 NAT 规则被清理
- 宿主机走了代理但容器没有
排查命令:
# 按顺序测试
docker exec <id> ping 8.8.8.8 # IP 层连通性
docker exec <id> ping baidu.com # DNS 解析
docker exec <id> curl -v https://google.com # 应用层
# 检查 Docker iptables NAT 规则
iptables -t nat -L -n | grep DOCKER
# 检查 MTU
docker network inspect bridge | grep -i mtu
三、时间与数据
坑二:容器时间不一致
现象: 容器内 date 与宿主机差 8 小时;程序日志时间戳不符;数据库写入时间错误。
原理: 容器默认使用宿主机 kernel 但没有自己的时区设置。如果宿主机是 CST(UTC+8),容器没挂载时区文件就会用 UTC。
修复方案:
# 运行时挂载时区文件
docker run -v /etc/timezone:/etc/timezone:ro \
-v /etc/localtime:/etc/localtime:ro nginx
# 设置环境变量(部分基础镜像支持)
docker run -e TZ=Asia/Shanghai nginx
坑六:数据未持久化导致丢失
现象: 重新部署容器后数据找不到;数据库容器重启后变空库;配置文件修改后重启恢复默认。
原理: 默认容器文件系统是「写时复制」(copy-on-write),容器删除后这一层就没了。必须显式使用数据卷(Volume)、绑定挂载(Bind Mount)或 tmpfs。
# 正确:使用命名数据卷
services:
mysql:
image: mysql:8.0
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
driver: local
错误做法: 使用匿名 volume(volumes: - /var/lib/mysql),重启后数据可能丢失。
四、容器管理
坑七:镜像标签混乱(latest 陷阱)
现象: docker run my-app:latest 版本不对;部署后发现不是预期版本;本地 latest 和远程 latest 不同。
原理: latest 只是一个普通 tag,不自动指向最新版本。它的指向完全取决于最后一次 docker build -t 或 docker tag 操作。
修复方案: 始终使用具体版本标签,不要用 latest。推荐 GitOps 流程:git commit SHA 作为镜像标签。
坑八:PID 1 信号处理问题
现象: docker stop 超时,容器无法优雅停止;日志显示进程退出码 0 但子进程变僵尸。
原理: 容器内的 PID 1 进程对信号处理有特殊要求。如果 PID 1 是 shell 脚本(如 CMD ["/bin/sh", "-c", "java -jar app.jar"]),shell 不转发 SIGTERM。
修复方案:
# 正确:exec 形式 CMD,让信号直接发给应用
CMD ["java", "-jar", "app.jar"]
# 或使用 init 进程(Docker 20.10+ 内置 tini)
docker run --init my-app:latest
五、安全加固
坑十:Docker daemon 安全暴露(2375/2376 端口)
现象: 服务器被入侵,挖矿程序通过 Docker 逃逸到宿主机;公网任何人可通过 docker -H tcp://server:2375 ps 操作远程容器。
原理: Docker daemon 默认不开放 TCP 端口。若管理员暴露到公网,任何能访问该端口的人都能以 root 权限运行任意容器→读取所有文件→通过逃逸获宿主机 root。
排查命令:
# 检查 Docker daemon 监听端口
ps aux | grep dockerd | grep -v grep
ss -tlnp | grep docker
# 测试是否对外开放
curl http://localhost:2375/info
修复方案:
# 立即关闭暴露的 Docker API
# 修改 systemd unit,不要加 -H tcp://0.0.0.0:2375
systemctl daemon-reload && systemctl restart docker
如确实需要远程管理,必须使用 TLS 证书认证+网络层限制(防火墙仅允许管理网段访问)。
参见 server-security-hardening-checklist 的服务器安全加固总纲,以及 ssh-brute-force-protection-guide 的访问控制实践。
六、易被忽视的 5 个坑
坑十一:容器时区问题(与坑二重申,详见上文)
坑十二:忘记 --restart 策略
# 生产环境建议
docker run -d --restart=unless-stopped my-app:latest
推荐策略对比:
| 策略 | 行为 |
|------|------|
| no | 不自动重启(默认) |
| on-failure:3 | 非零退出码时重启,最多 3 次 |
| always | 始终重启,dockerd 重启后也会重启 |
| unless-stopped | 始终重启,但 dockerd 重启前手动停掉的不会自动重启 |
坑十三:数据卷权限问题
挂载宿主机目录给容器,容器内进程以非 root 用户运行时无权限读取宿主机文件。
解决方案: 修改宿主机目录权限;或在 Dockerfile 中创建用户并设置正确 UID/GID。
坑十四:跨容器网络通信(bridge vs host)
# 推荐:创建自定义 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
# 注意:--link 已废弃,不要再使用
坑十五:多阶段构建泄露敏感信息
# 错误:构建阶段凭证带入最终镜像
FROM golang:1.21 AS builder
COPY . /app
RUN go build -o app .
FROM alpine
COPY --from=builder /root/.npm /root/.npm # 泄露了 npm 凭证
# 正确:只复制最终产物
FROM alpine
COPY --from=builder /app/app /app
关联页面
| 页面 | 关联点 |
|---|---|
| container-networking-troubleshooting | | 容器网络排障 6 层模型(Docker bridge 网络是第 ②~⑤ 层) | | 页面 | 关联点 | |------|--------| | |
| docker-image-optimization | | Docker 镜像优化总纲(镜像瘦身/多阶段构建) | | |
| go-static-compilation-docker | | Go 静态编译+scratch 极致瘦身(多阶段构建最佳实践) | | |
| k8s-resource-limits-configuration | | K8s 资源限制 vs Docker 原生限制(OOMKill 跨层分析) | | |
| resource-rbac-scheduling-troubleshooting | | K8s 资源配额/OOMKilled 排障 | | |
| server-security-hardening-checklist | | 服务器安全加固总纲(含 Docker daemon 安全) | | |
| tcp-connection-attack-vs-bug | | TCP 连接数爆表排查:攻击 vs Bug | | |
| jenkins-ansible-integration-guide | | Jenkins + Ansible 集成实战指南 — Ubuntu 24.04 环境安装、插件配置、 | | |
| jvm-container-oom-offheap-troubleshooting | | JVM 堆外内存(DirectByteBuffer/Metaspace/线程栈)导致容器 OOMKi | | |
| k8s-capacity-planning-qos-cost-optimization | | K8s 容量规划方法论与成本优化 — 从流量到资源预算的完整框架,含 QoS 策略、弹性伸缩协同、落 | | |
| pod-troubleshooting | | Pod CrashLoopBackOff 排障(含 Java OOM/Exit Code 分析) | |