凌晨两点,CI流水线又挂了。构建日志里全是npm install的进度条,而你只改了一行注释。

问题不在网络,在你的Dockerfile顺序。

打开网易新闻 查看精彩图片

镜像不是文件,是千层饼

多数人以为docker build是在"编译应用",其实是在叠文件系统快照。

每个指令生成一层:FROM打底、RUN拍快照、COPY塞文件、ENV写元数据。这些层只读、用SHA256寻址、能被别的镜像复用。

关键设计:层一旦写好,永不修改。改了就叠新层,旧层原地不动。

这是Docker快的核心,也是你构建慢的元凶。

缓存失效的残酷法则

Docker有一条铁律:某层变了,它上面所有层全部作废。

看看这个经典反例:

FROM node:20COPY . . # 代码天天变RUN npm install # 被迫每次都跑

代码一动,依赖重装。团队越大,浪费越惊人。

调个顺序,世界清静:

FROM node:20COPY package*.json ./ # 很少变RUN npm install # 大多时候直接命中缓存COPY . .

口诀:稳的放上面,变的沉底。

多阶段构建:把垃圾留在门外

编译工具、测试依赖、源码——这些不该出现在生产镜像里。

BuildKit的做法是分阶段:

# 构建阶段FROM node:20 AS builderWORKDIR /appCOPY package*.json ./RUN npm installCOPY . .RUN npm run build# 生产阶段FROM node:20-alpineWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCMD ["node", "dist/server.js"]

最终镜像只有dist和node_modules,builder阶段的一切都被丢弃。

体积更小、攻击面更小、部署更快。

BuildKit:现在已经是默认答案

老构建器逐行执行,BuildKit能并行、能缓存挂载、能按需加载。

启用方式:

DOCKER_BUILDKIT=1 docker build .

新版本Docker已默认开启,但老旧CI环境可能还在用 legacy builder。

排查慢构建,先确认引擎版本。

调试三板斧

构建慢?检查指令顺序,把稳定依赖往上挪。

缓存诡异失效?docker build --no-cache强制重来,排除干扰。

镜像膨胀?多阶段配合.dockerignore,把不需要的文件挡在构建上下文外。

想看底层细节?docker build --progress=plain .会暴露每一层的实际执行。

文件系统怎么"叠"起来的

Docker用联合文件系统(Union File System,比如OverlayFS)合并各层。

底层只读,顶层可写。你看到的是完整文件系统,内部是多层叠加。

读文件时从上往下找,写文件时复制到顶层再修改(写时复制机制)。

这种设计让镜像分发极高效:相同层只传一次,本地直接复用。

你的Dockerfile是性能蓝图

它不是命令清单,是缓存策略的具象化表达。

层序决定速度,阶段决定体积,上下文决定可重复性。

团队里第一个搞懂这套的人,通常会成为"那个优化构建的"。

下次写Dockerfile前,先画个依赖变更频率图——最静的放最上,最躁的沉到底。你的CI账单会感谢你。

你们团队的构建时间目前是多少?有没有试过把package.json单独COPY一层后,缓存命中率变化有多大?