讲解

docker build 的每条指令都会产生缓存层:指令文本和它引用的文件内容没变,就直接复用旧层,秒级完成;任何一层变了,它之后的所有层全部重建。这条「变化即失效、失效即传染」的规则是优化 Dockerfile 的总纲:把「很少变」的指令放前面(基础镜像、系统依赖),把「天天变」的指令放后面(业务代码 COPY)。

最经典的优化是依赖层与代码层分离。反面教材是 COPY . . 然后 RUN npm ci——任何代码改动都让依赖重装。正确姿势:先 COPY package.json package-lock.json 单独成层并 RUN npm ci,再 COPY 其余源码——代码改动只重建代码层,依赖层稳如泰山。这个「先清单后源码」的模式适用于所有语言(go.mod/go.sum、requirements.txt、pom.xml)。

.dockerignore 是另一个必须有的文件,语法类似 .gitignore,决定哪些文件不进构建上下文。作用有二:提速(node_modules、.git、日志不必打包上传给守护进程)和安全(.env、密钥文件绝不能进上下文,进了就可能被 COPY 进镜像)。常见清单:.git、node_modules、dist、*.log、.env、Dockerfile 本身、CI 目录。另外注意 .dockerignore 排除的文件 COPY 也拿不到——「构建需要但不该进镜像」的文件要靠多阶段或 secret 挂载(--mount=type=secret)解决。

示例

构建上下文:模拟「依赖清单 + 源码」结构的 Node 项目:

mkdir -p cachectx
cat > cachectx/package.json <<'EOF'
{ "name": "cache-demo", "version": "1.0.0", "main": "index.js", "scripts": { "start": "node index.js" } }
EOF
printf 'console.log("v1: 第一次构建");\n' > cachectx/index.js
printf 'node_modules\n.git\n*.log\n.env\n' > cachectx/.dockerignore

依赖层先行的 Dockerfile(无外部依赖,用 npm install --omit=dev 演示分层姿势):

# context: cachectx
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY index.js ./
CMD ["node", "index.js"]

第一次构建(全量),然后改源码再构建——第二次的依赖层全部命中缓存(CACHED 标记),只有代码层重建:

cd cachectx && docker build -t ohmydocs-verify-cache:1.0 . 
printf 'console.log("v2: 只改了源码");\n' > index.js
docker build -t ohmydocs-verify-cache:2.0 . 2>&1 | grep -E 'CACHED|DONE' | head -8
cd ..

两个版本的镜像各自正常运行,验证缓存没破坏正确性:

docker run --rm ohmydocs-verify-cache:1.0
docker run --rm ohmydocs-verify-cache:2.0

常见坑

  • COPY . . 在依赖安装之前:最常见的缓存杀手;任何语言都先抄「清单先行」模式。
  • .dockerignore 忘配 .env:密钥文件被 COPY 进镜像层,删指令也救不回历史层——换密钥并重建是唯一解。
  • 以为删文件能缩小镜像:RUN rm 只是在新层里标记删除,旧层里的数据还在;要缩体积得在同一层里「装完即清」。
  • 调试时缓存捣乱:改了远程脚本但 ADD/RUN curl 层命中缓存没重跑;用 --no-cache 或给 URL 加版本参数。

小结

缓存规则:变一层、重建后面全部;依赖清单先于源码 COPY;.dockerignore 既提速又防泄密。下一章进入多容器编排:Compose。