Docker 把“在我机器上能跑”变成了“在任意机器上都能跑”,但它不是免死金牌。本文复盘 8 个真实生产雷区:镜像膨胀、时区错乱、PID 1 僵尸进程、数据卷权限、日志爆盘、健康检查假阳性、–rm 误删数据、ulimit/OOM。每个都附可复制的修复命令,建议收藏对照自查。如果你还在用 Docker Compose 一键编排 起步,更要留意最后的“状态型 vs 临时型”容器边界。
这些坑之所以反复出现,是因为它们大多藏在“能跑就行”的侥幸里:本地 docker run 测一下没问题,一上生产就暴露。下面按“现象 → 根因 → 修复”逐条拆解,建议对照自己的镜像和编排文件自查一遍,别等事故当天才翻这篇。
雷区一:镜像膨胀,构建一次传十分钟
最常见的反模式是把整个项目 COPY 进镜像,再 RUN npm install / pip install,结果一层就吃掉 1.5GB,且每次改一行代码都要重装全部依赖。根因是没利用层缓存,也没隔离“依赖”和“源码”。
# 多阶段构建:构建阶段产物只复制必要产物到运行阶段
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
配合 .dockerignore 排除 node_modules、.git、本地日志,构建体积可降一个数量级。注意 COPY package*.json 必须放在 COPY . . 之前,依赖层才能命中缓存。经验值:一个生产镜像尽量控制在 200MB 以内,基础镜像越精简、层数越少,拉取与回滚都越快;用 docker history 可以一眼看出哪一层最胖。
# .dockerignore
node_modules
.git
*.log
dist
.env
雷区二:时区错乱,日志时间对不上
容器默认 UTC,国内业务日志比实际晚 8 小时,排障时和宿主机时间对不上。别在代码里硬编码时区,应在镜像或运行时统一设置。
# 运行时直接注入
docker run -e TZ=Asia/Shanghai myapp:1.0
# 或在 Dockerfile 固化(Debian 系)
RUN apt-get update && apt-get install -y tzdata \\
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \\
&& echo "Asia/Shanghai" > /etc/timezone
雷区三:PID 1 僵尸进程与信号丢失
容器内 1 号进程若不是 init 系统,子进程退出的“僵尸态”没人回收;更隐蔽的是 SIGTERM 只发给 PID 1,普通应用进程收不到,导致 docker stop 等满 10 秒才被 SIGKILL 强杀、优雅关闭失效。Java/Python 尤其容易踩。
# 方案 A:用 tini 作 1 号进程(推荐)
FROM node:20
RUN apt-get update && apt-get install -y tini
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "server.js"]
# 方案 B:shell 形式改为 exec 形式,让应用直接成为 PID 1
# 错误:CMD node server.js (sh 成为 PID 1)
# 正确:CMD ["node", "server.js"]
雷区四:数据卷权限,挂载后应用写不进
把宿主机目录挂进容器后,文件 owner 是 root,而应用以普通用户(如 uid 1001)运行,启动时直接 Permission denied。很多人的第一反应是 chmod 777,这既不安全也治标不治本。
# 方案 A:运行时指定与宿主机一致的 uid/gid
docker run -u $(id -u):$(id -g) -v $PWD/data:/app/data myapp
# 方案 B:entrypoint 里按需 chown(适合带初始化脚本的镜像)
#!/bin/sh
chown -R appuser:appuser /app/data
exec gosu appuser node server.js
雷区五:容器日志无限增长,磁盘被吃满
默认 json-file 日志驱动没有上限,疯狂打印的容器几天就能写满磁盘,连带拖垮同宿主的其他服务(参见 Linux 磁盘空间爆满排查)。生产环境务必全局限制。
# /etc/docker/daemon.json 全局配置
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
# 改完重启:systemctl restart docker
雷区六:健康检查假阳性
HEALTHCHECK 只探测端口通不通,进程其实已经死锁或连接池耗尽,编排系统却一直认为它“健康”,流量照常打进来。健康检查必须探“业务能响应”,而非“端口在监听”。
# 错误:只探端口
HEALTHCHECK CMD curl -f http://localhost:8080/ || exit 1
# 正确:探真实业务接口(注意基础镜像需有 wget/curl)
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \\
CMD wget -qO- http://localhost:8080/health >/dev/null 2>&1 || exit 1
雷区七:–rm 误删数据与重启丢失
docker run --rm 退出即删容器,临时调试很方便,但有人把数据库、上传目录也跑在无卷的 --rm 容器里,一停全没。还有人以为“容器重启数据还在”,结果数据写在容器可写层,重建镜像后荡然无存。务必区分临时型与状态型容器。
# 状态型数据必须挂 named volume
docker volume create pgdata
docker run -d --name pg \\
-v pgdata:/var/lib/postgresql/data \\
-e POSTGRES_PASSWORD=**** postgres:16
# 临时调试用完即焚,绝不存关键数据
docker run --rm -it alpine sh
雷区八:ulimit / OOM,被宿主机默默杀掉
不设内存上限的容器,内存泄漏时会把整个宿主拖垮(正如 Java 内存泄漏排查复盘 中的堆飙升场景)。同时文件描述符 ulimit 过低,高并发下会报 “too many open files”。这些与 Linux 内核参数调优 一脉相承,需在容器与宿主两端同时收敛。
# 运行时限制内存与 swap
docker run -d --memory=1g --memory-swap=1g --name api myapi:1.0
# docker-compose 等价写法
services:
api:
image: myapi:1.0
deploy:
resources:
limits:
memory: 1g
ulimits:
nofile:
soft: 65536
hard: 65536
八雷区速查表
| 雷区 | 典型现象 | 修复要点 |
| 镜像膨胀 | 构建慢、传镜像十分钟 | 多阶段构建 + .dockerignore |
| 时区错乱 | 日志晚 8 小时 | 注入 TZ=Asia/Shanghai |
| PID 1 僵尸 | 优雅关闭失效 | tini / exec 形式 |
| 卷权限 | Permission denied | -u 指定 uid 或 entrypoint chown |
| 日志爆盘 | 磁盘被写满 | daemon.json log-opt 限大小 |
| 健康假阳性 | 端口通但业务挂 | 探真实业务接口 |
| –rm 误删 | 数据一键蒸发 | 状态型数据挂 volume |
| OOM/ulimit | 被宿主强杀 | –memory 限内存 + ulimit |
上线前 Docker 自查清单
把以上八条固化成上线前 checklist,可以挡掉绝大多数容器事故:① 镜像是否多阶段构建、是否带 .dockerignore;② 是否显式设置 TZ;③ 1 号进程是否为 init(tini)或 exec 形式;④ 状态型数据是否全部挂 volume,绝不落在可写层;⑤ 日志是否限制了 max-size;⑥ 健康检查是否探测真实业务接口;⑦ 是否设置了 --memory 与 ulimit;⑧ 挂载目录的 owner 是否与运行用户一致。把这八项写进 CI 流水线 的扫描步骤,比事后救火省心得多。
另外提醒:很多人把 Docker 当成“轻量虚拟机”,习惯进容器改配置、装软件,结果一重建镜像改动全丢。正确姿势是“镜像不可变”——所有变更走 Dockerfile 与配置挂载,容器随时可销毁重建。配合镜像扫描(如 Trivy)定期查 CVE,容器安全才算闭环,这点在 Linux 服务器安全加固 里也有呼应。
小结
这 8 个雷区大多不是 Docker 的 bug,而是“把它当黑盒”的代价。把镜像分层、时区、信号、卷、日志、健康、资源限制当成上线 checklist,配合 GitHub Actions 自动化构建 固化到 CI,就能让容器真正稳定可控。踩坑不可怕,可怕的是同一个坑反复踩——把这篇当成团队的容器上线校验单就好。你最常被哪个雷区坑过?欢迎在评论区交流。




