背景
Docker 已经是现代运维和开发的基础设施,但在生产环境中使用 Docker,由于环境的复杂性和容器的特殊性,很多在物理机或虚拟机上不会出的问题在容器环境下会集中爆发。本篇从实际生产故障中提炼出 10 个最容易踩的坑,每个坑都给出真实的现象描述、原理说明、排查命令和修复方案。
这些坑覆盖了镜像管理、容器生命周期、网络配置、存储管理、安全加固、监控告警等 Docker 使用中最常见的领域。
坑一:Docker 存储空间耗尽(Disk Full)
现象
- 容器无法启动,日志报错
no space left on device docker ps报错Cannot connect to the Docker daemondf -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/<container-id>/*-json.log
预防措施
- 把
/var/lib/docker放在独立分区或 LVM 逻辑卷 - 配置容器日志轮转(
max-size+max-file) - 定期清理镜像和构建缓存
- 监控磁盘使用率,超过 80% 告警
坑二:容器内时间与宿主机时间不一致
现象
- 容器内
date命令输出和宿主机差 8 小时 - 程序日志时间戳和实际时间不符
- 数据库写入的时间差了 8 小时
- 证书有效期计算错误
原理
容器默认使用宿主机的 kernel,没有自己的时区设置。如果宿主机是 CST(UTC+8),但容器没有正确挂载时区文件,就会使用 UTC 时间。
排查命令
# 查看宿主机时间
date
# 查看容器内时间
docker exec <container-id> date
# 检查容器是否挂载了时区文件
docker inspect <container-id> | 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 exec <container-id> cat /etc/resolv.conf
# 查看容器网络模式
docker inspect <container-id> | grep -A 10 "NetworkSettings"
# 从容器内测试 DNS
docker exec <container-id> nslookup nginx
docker exec <container-id> 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 <container-id>最后一条日志正常,没有错误信息docker inspect <container-id>显示OOMKilled: true- 宿主机
dmesg | grep -i oom或journalctl | grep -i oom有记录
原理
容器的内存限制由 Linux cgroup 控制。当容器内的进程试图申请超过 limit 的内存时,Linux 会触发 OOM Killer 选择容器内的一个进程杀掉。如果进程没有处理 SIGKILL 信号,容器会直接退出。
排查命令
# 检查容器退出状态
docker inspect <container-id> | grep -E "OOMKilled|ExitCode|State"
# 查看容器内存使用峰值
docker stats <container-id> --no-stream
# 查看容器内存限制
docker inspect <container-id> | grep -A 5 "Memory"
# 在宿主机上查看 cgroup 内存统计
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/docker/<container-id>/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 exec <container-id> ping 8.8.8.8 # 测试 IP 层连通性
docker exec <container-id> ping baidu.com # 测试 DNS 解析
docker exec <container-id> 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 <container-id> | grep -A 20 "Mounts"
# 查看数据卷列表
docker volume ls
# 查看数据卷详情
docker volume inspect <volume-name>
# 检查宿主机上的数据卷路径
ls -la /var/lib/docker/volumes/<volume-name>/_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显示<none>- 部署后发现版本不对,查不到是哪个镜像
原理
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 <container-id>超时,无法优雅停止容器- 容器收到 SIGTERM 后没有优雅退出
docker kill <container-id>直接发送 SIGKILL,进程没机会做清理- 日志显示
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 exec <container-id> ps aux
# 查看 PID 1 的进程是什么
docker exec <container-id> cat /proc/1/cmdline | tr '\0' ' '
docker exec <container-id> ps -p 1
# 测试发送 SIGTERM 后的停止时间
time docker stop <container-id>
修复方案
方案一:使用 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}}\t{{.Image}}\t{{.Status}}\t{{.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 <server-ip>
修复方案
立即关闭暴露的 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: 非零退出码时最多重启 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 故障排查卡”,关键时刻能省大量时间。


直接扫描上方二维码,备注【Docker指令】,即可100%发给你!
本文链接:https://www.yunweipai.com/49277.html





网友评论comments