首页 Docker教程别再踩坑!生产环境 Docker 最易中招的 10 个问题

别再踩坑!生产环境 Docker 最易中招的 10 个问题

运维派隶属马哥教育旗下专业运维社区,是国内成立最早的IT运维技术社区,欢迎关注公众号:yunweipai
领取学习更多免费Linux云计算、Python、Docker、K8s教程关注公众号:马哥linux运维

背景

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/<container-id>/*-json.log

预防措施

  1. 把 /var/lib/docker 放在独立分区或 LVM 逻辑卷
  2. 配置容器日志轮转(max-size + max-file
  3. 定期清理镜像和构建缓存
  4. 监控磁盘使用率,超过 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. 如果是正常业务增长,考虑水平扩展(多容器实例)

预防措施

  1. 设置合理的内存 limit,不要设置得过高或过低
  2. Java/Node.js 等有自己内存管理的应用,要明确设置堆内存
  3. 配置监控告警:内存使用率超过 80% limit 时告警
  4. 在宿主机部署 OOM 告警脚本

坑五:容器无法访问外网,但宿主机正常

现象

  • ping baidu.com 在宿主机正常,容器内不通
  • curl https://google.com 在宿主机正常,容器内超时
  • 容器间通信正常(同一个 bridge 网络内)
  • 从容器内访问宿主机 IP 正常,但访问其他 IP 不通

原理

这种情况通常是 Docker 的包过滤(iptables/IPVS)或网络 MTU 问题导致的。常见原因:

  1. MTU 不匹配:容器默认使用 docker0 网桥的 MTU(默认 1500),如果宿主机网卡的 MTU 是 9000(jumbo frame),路径 MTU 发现可能失败
  2. iptables 规则被意外修改:Docker 会自动添加 iptables NAT 规则,如果被清理,容器无法通过 NAT 访问外网
  3. 宿主机开启了数据包转发但 Docker 没正确配置
  4. 代理设置:宿主机走了代理但容器没有

排查命令

# 检查容器网络连通性(按顺序测试)
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 进程,但:

  1. 如果 PID 1 进程是 shell 脚本(如 CMD ["/bin/sh", "-c", "java -jar app.jar"]),shell 本身不转发信号,Java 进程收不到 SIGTERM
  2. 如果 PID 1 进程没有正确处理 SIGTERM,容器会一直等直到 timeout(默认 10 秒),然后被 SIGKILL
  3. 某些基础镜像的 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+,所有容器响应极慢

原理

没有资源限制的容器理论上可以使用宿主机全部资源。当一个容器出问题(如内存泄漏),它会吸干宿主机的内存,导致:

  1. 其他容器因内存不足被 OOMKill
  2. Docker daemon 本身也可能受影响
  3. 宿主机 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),任何能访问到这个端口的人都可以:

  1. 在宿主机上以 root 权限运行任意容器
  2. 读取宿主机的所有文件
  3. 通过容器逃逸获得宿主机 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

容器安全最佳实践

  1. 永远不要把 Docker API 端口暴露到公网
  2. 使用 --read-only 模式运行容器,防止写入恶意文件
  3. 使用 --security-opt=no-new-privileges 防止特权升级
  4. 不要用 --privileged 运行容器
  5. 定期审查容器能力: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 最易中招的 10 个问题插图
别再踩坑!生产环境 Docker 最易中招的 10 个问题插图1

直接扫描上方二维码,备注【Docker指令】,即可100%发给你!

本文链接:https://www.yunweipai.com/49277.html

网友评论comments

发表回复

您的电子邮箱地址不会被公开。

暂无评论

Copyright © 2012-2022 YUNWEIPAI.COM - 运维派 京ICP备16064699号-6
扫二维码
扫二维码
返回顶部