构建镜像与构建缓存机制
本教程共 25 篇 · 第 13 篇 · 更新于 2026-07-26 · 约 9 分钟阅读
13. 构建镜像与构建缓存机制
本节目标:搞懂镜像分层和构建缓存,学会调整 Dockerfile 指令顺序让构建又快又省。
你有没有遇到过这种情况:第一次构建镜像要好几分钟,改了一行代码再构建,居然几秒就完了?或者反过来,明明只改了个注释,构建却把所有依赖重装了一遍?
这背后就是「构建缓存」在起作用。理解它,你就能写出构建飞快的 Dockerfile;不理解,就只能在漫长的等待里怀疑人生。
镜像是一层一层堆出来的
先回顾一下镜像的本质:它不是一整块文件,而是由多个只读层(layer)叠起来的。
每一层对应 Dockerfile 里的一条指令。比如这个 Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "./src/index.js"]
构建出来大致是这样:
- 第 1 层:
node:22-alpine基础镜像(本身也是多层叠的) - 第 2 层:
WORKDIR /app创建工作目录 - 第 3 层:
COPY . .拷进源码 - 第 4 层:
RUN yarn install装依赖 - 第 5 层:
CMD设启动命令(这层几乎不占空间)
层之间是叠加关系,像透明胶片一样摞起来,组成容器看到的完整文件系统。
Note层一旦创建就不可变(immutable)。这也意味着层可以被复用—两个镜像如果共享同一批底层,Docker 只存一份,省磁盘、省带宽。
构建缓存:能复用就复用
构建缓存的核心思想很简单:如果某一层的输入没变,输出就一定一样,直接用上次的结果,跳过实际执行。
构建时,Docker 逐条处理 Dockerfile,对每条指令先检查「这一层能不能用缓存」。判断依据是指令本身和它依赖的上下文有没有变。如果能用,就显示 CACHED,瞬间完成;不能用,就重新执行,并且从这条往后的所有层都跟着失效。
第一次构建没有缓存可用,每层都得跑。第二次构建如果啥都没改,全命中缓存,几秒就完事:
$ docker build .
[+] Building 1.0s (9/9) FINISHED
=> CACHED [2/4] WORKDIR /app 0.0s
=> CACHED [3/4] COPY . . 0.0s
=> CACHED [4/4] RUN yarn install 0.0s
对比第一次的 20 秒,缓存命中后只要 1 秒,这就是缓存的威力。
缓存什么时候失效
哪些改动会让缓存失效?记住三条:
- 指令本身变了。 比如
RUN apt-get install -y curl改成RUN apt-get install -y curl git,这条RUN的缓存就废了。 - 被拷贝的文件变了。
COPY . .拷的文件里只要有任何一个内容或权限变了,这层缓存就失效。 - 前面的层失效了。 缓存是链式的,一旦某层失效,它后面的所有层都跟着失效,哪怕后面的指令没动。
第三条最关键,也是新手最容易踩的坑。看这个例子:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "./src/index.js"]
COPY . . 拷的是整个项目目录。你只改了 src/index.js 里的一行代码,COPY 这层就失效了。然后 RUN yarn install 跟着失效,依赖全部重装。明明依赖一个都没变,却要花十几秒重新安装,纯属浪费。
调整指令顺序,让缓存更耐用
解决办法是调整 Dockerfile 的指令顺序:把变化少的放前面,变化多的放后面。
依赖声明文件(package.json、requirements.txt、go.mod)很少变,而源码经常变。所以先把依赖文件单独拷进去,装完依赖,再拷源码:
FROM node:22-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
EXPOSE 3000
CMD ["node", "src/index.js"]
这样改之后,只要 package.json 不变,RUN yarn install 就一直命中缓存。你天天改源码,依赖安装那层始终秒过,构建只在最后的 COPY . . 那一层重新执行。
Tip这个技巧叫「先拷依赖清单,再装依赖,最后拷源码」,几乎所有语言通用:Python 拷
requirements.txt、Java 拷pom.xml、Go 拷go.mod、Ruby 拷Gemfile。记住这个套路,构建速度能差好几倍。
.dockerignore:别把垃圾传给 Docker
前面说过,COPY . . 会把构建上下文整个拷进去。如果上下文里有 node_modules、.git、日志文件、构建产物,它们也会被拷进去,既拖慢构建,又污染镜像。
解决办法是在项目根目录放一个 .dockerignore 文件,列出要排除的内容:
node_modules
.git
*.log
dist
build
.env
npm-debug.log
.DS_Store
它的语法和 .gitignore 类似。构建时 Docker 会跳过这些文件,不打包进上下文。
.dockerignore 有两个直接好处:
- 构建上下文变小,传给守护进程更快。
- 缓存更稳定,避免无关文件变动导致
COPY层缓存失效。
Warning如果不忽略
node_modules,本地的node_modules会被拷进镜像,覆盖掉容器里yarn install装的依赖。本地是 Mac、容器是 Linux,二进制不兼容,跑起来就崩。这是 Node 项目最常见的玄学报错根源之一。
合并 RUN 层,减少镜像体积
每条 RUN、COPY、ADD 都会产生一个新层。层越多,镜像越大。
把多个相关操作合并到一个 RUN 里,能减少层数:
# 不好:三条 RUN,三层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# 好:一条 RUN,一层,且清理在同一层
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
这里有个反直觉的点:如果装包和删缓存不在同一层,删了也没用—删缓存那一层只是「标记删除」,底层那层的数据还在,镜像体积不会小。必须在同一个 RUN 里装完就删,才能真正瘦身。
看清镜像的层结构
想看一个镜像到底有哪些层、每层多大,用 docker image history:
docker image history my-node-app
输出类似:
IMAGE CREATED CREATED BY SIZE
f279389d5f01 8 seconds ago CMD ["node" "./src/index.js"] 0B
<missing> 8 seconds ago EXPOSE map[3000/tcp:{}] 0B
<missing> 8 seconds ago COPY . . 450kB
<missing> 8 seconds ago RUN yarn install --production 126MB
<missing> 8 seconds ago COPY package.json yarn.lock ./ 1.2kB
<missing> 8 seconds ago WORKDIR /app 8kB
<missing> 4 days ago /bin/sh -c #(nop) CMD ["node"] 0B
每一行就是一层,能看到创建命令和占用大小。哪层臃肿一目了然,针对性优化即可。
强制不用缓存
有时候你怀疑缓存有问题,想从头构建一遍。加 --no-cache 参数:
docker build --no-cache -t my-app .
这条命令会忽略所有缓存,每层都重新执行。CI 流水线里常用来保证构建的纯净,但日常开发用它会慢很多,别习惯性加。
如果只想重新执行某一层,可以在那条指令前做个小改动(比如改个注释),让它失效即可,不用全盘重来。
小结:构建优化的几条原则
把这一章浓缩成几条可操作的原则:
- 指令顺序按「变化频率」排:少变的(依赖清单)在前,多变的(源码)在后。
- 善用
.dockerignore:排除node_modules、.git、日志、构建产物。 - 合并
RUN层:相关操作用&&串起来,并在同一层里清理缓存。 - 钉死基础镜像版本:用
python:3.13.1而不是python:latest,保证构建可复现。 COPY优于ADD:行为简单可控。
NoteDocker Engine 29.x 默认启用 BuildKit 构建器,缓存机制比老版本更智能,还会并行构建没有依赖关系的层。你基本不需要手动开启,但如果你看到构建日志是
[+] Building这种树状输出,就说明 BuildKit 已经在工作了。
下一章我们讲多阶段构建—怎么把动辄几百 MB 的镜像,精简到几十 MB。