生产环境中使用Docker的常见问题分析

描述

背景

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 exec  date

# 检查容器是否挂载了时区文件
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 exec  cat /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  显示 OOMKilled: true

宿主机 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 exec  ping 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  直接发送 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  ps 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 故障排查卡",关键时刻能省大量时间。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分