Appearance
生产环境踩坑记录
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 还支持 syslog、journald、local、fluentd 等日志驱动。生产环境更常见的做法是通过日志采集工具(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.sockCI 工具(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 生产问题通常不是某个命令不会用,而是这些外围条件没有提前落到配置里。容器运行状态、宿主机状态和业务状态要串起来看,单看哪一层都可能漏掉根因。