去年我们组接了一个老项目,前任工程师留下一份 Dockerfile 和一句话:"能跑,别动"。
我没忍住,动了。
然后接下来三个月,我和 Docker 的关系从"工具人"变成了"冤家"。下面这 5 个坑,有的让我重做了镜像,有的让我加班到凌晨,有的让我们的 CI 跑了 40 分钟才发现是个空气问题。

环境:Docker 24.0,docker-compose v2.21,主要在 Linux(Ubuntu 22.04)和 Mac M2 上跑。
坑 1:Dockerfile 里 COPY 顺序写错,缓存全废
最早写 Dockerfile,我图省事,这么写:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD
"node", "dist/index.js"
看着挺好对吧?能跑。
问题是,每次我改了一行业务代码,COPY . . 这一层的缓存就失效了。下面的 npm install 重新跑——3 分钟。npm run build 重新跑——2 分钟。每次推送都要 5 分钟以上构建。
写 Dockerfile 的核心规则我后来才搞明白:变得越频繁的东西,放越后面。
FROM node:20-alpine
WORKDIR /app
# package.json 变得不频繁,先 copy
COPY package*.json ./
RUN npm install # 只要 package.json 没变,这一步走缓存
# 业务代码经常变,放后面
COPY . .
RUN npm run build
CMD
"node", "dist/index.js"
改完之后,日常修代码的构建时间从 5 分钟变成 30 秒。npm install 那一层只要 lockfile 没变就走缓存。
这是 Dockerfile 里最值钱的一条规矩。没有之一。
坑 2:镜像大小爆炸,根因是 npm install 留下的垃圾
第一版镜像我打出来 1.2GB,我以为是 Node 自带的。
后来加上 docker history,发现 npm install 那层就贡献了 800MB。npm 在 install 之后会留下大量缓存和构建依赖,生产环境根本用不上。
# 多阶段构建,builder 装齐所有依赖去构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 运行阶段,只装生产依赖
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
CMD
"node", "dist/index.js"
改完镜像从 1.2GB 降到 280MB。
几个细节:
如果是 Python 项目,对应的招数是 pip install --no-cache-dir,然后多阶段构建。
坑 3:容器里时区是 UTC,日志全错
某天凌晨我们的告警邮件标题写着 2026-04-08 19:23:11 ERROR ...,我一脸懵——那会儿明明是凌晨 3 点多。
原因是镜像基于 alpine,容器里默认 UTC 时区。我们打的日志带时间戳,但时间是 UTC,跟我们北京时间差 8 小时。告警邮件读起来一团乱。
# alpine 上要先装 tzdata
RUN apk add --no-cache tzdata && \
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
或者更简洁:
ENV TZ=Asia/Shanghai
RUN apk add --no-cache tzdata
如果你用 debian-based(node:20、python:3.11)这种,默认就装了 tzdata,直接 ENV TZ=Asia/Shanghai 就行。
这个坑特别隐蔽,因为本地用 docker 跑没问题(你 mount 了 host 的时区),只有上 K8s 或者纯净环境才会暴露。
坑 4:容器跑着跑着没了,根因是 OOM Killer
上线之后跑了 3 天,生产容器频繁重启。日志里啥错误都没有,容器就是突然死了。
排查思路:docker inspect 看 State,OOMKilled: true。
容器内存超出了限制,被宿主机的 OOM Killer 直接干掉。
Node.js 容易出这个问题,因为它默认堆大小是根据宿主机内存算的,不是容器内存限制。
# 你给容器分了 512MB,但 Node 以为自己有 8GB 内存可用
# 然后 V8 把堆撑到 4GB,直接被 OOM
解法:显式告诉 Node 它有多少内存。
ENV NODE_OPTIONS="--max-old-space-size=400"
注意要比容器内存限制留个 buffer。容器 512MB 的话,Node 堆给 400MB,留 100 多 MB 给 buffer、stack、native 库。
Java 应用类似,要传 -XX:MaxRAMPercentage=75 或者 -Xmx400m。
K8s 上,记得 resources 里写清 requests 和 limits,别只写 limits,不然调度可能让节点过载。
坑 5:`docker-compose down` 之后,数据没了
这是我自己踩的最蠢的一个。
本地开发环境用 docker-compose 起了一个 postgres,跑了一周,塞了不少测试数据进去。某天我嫌容器卡,顺手 docker-compose down -v。
-v 是删 volumes。一周数据,瞬间归零。
# docker-compose.yml
services:
db:
image: postgres:16
volumes:
pgdata:/var/lib/postgresql/data # 命名卷
environment:
POSTGRES_PASSWORD: dev
volumes:
pgdata:
docker-compose down(不带 -v)会停掉并删除容器,但保留 volumes。
docker-compose down -v 会连命名卷一起删。
我现在的习惯是:
另外,生产环境用 bind mount 比命名卷更稳——数据落到宿主机文件系统的具体路径,你能直接看到、能 rsync 备份、能在 Docker 之外管理:
volumes:
- /data/pg:/var/lib/postgresql/data
最后一个心得
写了三个月 Docker 后,我发现最大的认知转变是:
Docker 不是"另一种虚拟机"。它是一种把进程、文件、网络打包的方式。
很多人(包括早期的我)把 Docker 当 VM 用,容器里装一堆服务、SSH 进去改东西、用 docker exec 当日常运维入口。这些用法都偏离了它的设计意图,坑都会接踵而至。
容器应该是一次性的、无状态的、单进程的。改东西就重新打镜像、重新部署。这套思路转过来之后,大部分坑会自动消失。
写完这篇,我把它转给我们组那个还在 SSH 进容器改配置的同事看。结果他回我:"那不是更麻烦?"
我没回他。让他自己去踩。
版权声明
本文仅代表作者观点,不代表xx立场。
本文系作者授权xx发表,未经许可,不得转载。
最新留言