Skip to content

生产环境踩坑记录

Docker 在生产环境里出问题,往往不在"容器能不能启动",而在日志撑爆磁盘、资源没有限制、健康检查配错、镜像缓存堆积、存储驱动异常和权限边界不清。单个容器跑起来很简单,长期稳定运行要多看宿主机。

日志增长——容器还在跑,磁盘却满了

默认 json-file 日志驱动把容器标准输出写到宿主机文件里。应用持续输出日志、又没有轮转时,磁盘很容易被日志打满。

bash
docker inspect web --format '{{.LogPath}}'  # json-file 驱动下日志文件的宿主机路径

daemon 级别限制写在 /etc/docker/daemon.json

json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "5"
  }
}

重启 Docker 后这个配置只对新建容器生效。已经存在的容器要重建才会应用新的日志选项。日志策略要在服务上线前定好——磁盘满了再改,停服务和清理日志会变成同一个窗口里的两件事。

除了 json-file,Docker 还支持 syslogjournaldlocalfluentd 等日志驱动。生产环境更常见的做法是通过日志采集工具(Fluent Bit、Filebeat)把容器日志接到 Loki、ELK 或云日志服务里,Docker 本地日志只作为兜底。

资源限制——容器默认可以抢宿主机资源

不设资源限制时,一个容器能用掉整台机器的 CPU 和内存。单机跑多个服务时,每个服务的 CPU、内存、进程数都要设置:

bash
docker run -d \
  --name app \
  --cpus "2" \
  --memory "1g" \
  --pids-limit 512 \
  registry.example.com/ops/app:1.0
参数说明
--cpus "2"最多使用约 2 个 CPU 核
--memory "1g"内存硬上限 1GB,超限会被 OOM kill
--pids-limit 512限制容器内进程数量,防止 fork 炸弹

内存限制不是"建议值",是硬上限。Java、Node.js、Go 这类运行时如果不根据容器限制调整自身堆参数,会以为机器内存很大,实际被 cgroup 在背后 kill 掉。Java 的 -Xmx、Node.js 的 --max-old-space-size、Go 的 GOMEMLIMIT 都要和容器的 --memory 对齐。

OOM 排查

容器被 OOM kill 时,docker ps -a 可能看到退出码 137(128 + 9,即收到 SIGKILL):

bash
docker ps -a --filter name=app
docker inspect app --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
dmesg -T | grep -i 'killed process'  # 宿主机内核日志里也能看到 OOM 记录

排查方向:

方向说明
应用内存泄漏内存持续上涨,重启后暂时恢复
限制太小正常业务峰值也超过了 --memory
运行时参数未适配JVM heap、Node.js old space、Go GC 按宿主机内存设而非容器限制
宿主机整体内存不足不单是容器的问题,宿主机层面的内存压力

只靠"重启容器"处理 OOM,现场恢复了,原因没找到。复盘里至少要记录退出码、OOMKilled 状态、当时的内存曲线和容器限制值。

健康检查——端口通不等于服务可用

健康检查让 Docker 或编排系统知道服务是否真的可用,不只是进程在不在:

bash
docker run -d \
  --name app \
  --health-cmd 'curl -fsS http://127.0.0.1:8080/health || exit 1' \
  --health-interval 30s \
  --health-timeout 3s \
  --health-retries 3 \
  registry.example.com/ops/app:1.0

查看健康状态和历史:

bash
docker inspect app --format '{{.State.Health.Status}}'
docker inspect app --format '{{json .State.Health.Log}}'

健康检查不是业务压测。路径要轻——只检查进程能否处理基本请求和关键依赖的最小状态。把复杂 SQL、远程接口调用和慢逻辑放进健康检查,故障时健康检查本身挂掉,会让入口层更混乱。

重启策略——能重新拉起,不能修复根因

策略说明
no不自动重启
on-failure非 0 退出码时重启
always退出后总是重启
unless-stopped手工停止后不自动重启,其他情况都重启
bash
docker run -d \
  --name app \
  --restart unless-stopped \
  registry.example.com/ops/app:1.0

重启策略只能让容器进程重新拉起,不能修复配置错误、数据库不可用、权限不足。容器反复重启时,日志、退出码和最近一次配置变更更有价值——"容器又起来了"只能说明重启策略在生效。

存储驱动

当前 Linux 上最常见是 overlay2

bash
docker info | grep -E 'Storage Driver|Docker Root Dir'

常见存储层面的问题:

现象方向
镜像拉取失败/var/lib/docker 磁盘满
创建容器失败inode 用尽、overlay 挂载异常
容器写入慢可写层承担了大量写 IO
删除镜像不释放空间仍被容器引用、构建缓存未清理

大量写入的数据不适合放容器可写层。数据库、日志、上传文件放到 volume、bind mount 或外部存储,可写层只留给临时文件和中间产物。

Docker socket 风险

/var/run/docker.sock 是 Docker API socket。容器挂载了它,等于拿到了宿主机 Docker 控制权:

yaml
services:
  risky:
    image: docker:27
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

CI 工具(Jenkins、GitLab Runner)、监控工具(Portainer)有时需要这个 socket。使用前要确认容器来源可信、权限最小化、网络不暴露。这个 socket 一旦被恶意利用,可以启动特权容器、挂载宿主机任意目录,风险接近拿到宿主机 root。

高权限参数

参数风险
--privileged放开大量内核能力和设备访问
-v /:/host把宿主机根目录挂进容器
--network host网络隔离减弱
--pid host能看到宿主机进程命名空间
--cap-add额外增加 Linux capability

某些运维工具确实需要高权限(节点监控、网络排查、存储插件)。配置时把原因写清楚,能给单个 --cap-add=NET_ADMIN 就不直接 --privileged——单点能力比全开可控得多。

镜像和缓存堆积

bash
docker system df
docker system df -v         # 逐项细看
docker container prune      # 清理已停止的容器
docker image prune          # 清理悬空镜像
docker builder prune        # 清理构建缓存

清理 volume 要额外确认:

bash
docker volume ls
docker volume inspect <volume_name>

volume 往往存着业务数据。镜像缓存可以重拉,volume 删错就真的丢数据了。docker system prune -a 会清掉所有未被容器引用的镜像,生产机器上执行前务必确认当前版本和回滚版本都还在。

上线检查清单

方向检查点
镜像来源、标签、digest、扫描结果
启动入口命令、环境变量、配置挂载
网络端口绑定地址、防火墙、安全组
数据volume、bind mount、备份路径
资源CPU、内存、pids、ulimit
日志日志驱动、轮转、采集链路
健康healthcheck、重启策略、告警
权限root、capabilities、--privileged、Docker socket

Docker 生产问题通常不是某个命令不会用,而是这些外围条件没有提前落到配置里。容器运行状态、宿主机状态和业务状态要串起来看,单看哪一层都可能漏掉根因。