首页 / Docker 入门教程 / 构建镜像与构建缓存机制

Docker 入门教程

构建镜像与构建缓存机制

本教程共 25 篇 · 第 13 篇 · 更新于 2026-07-26 · 约 9 分钟阅读

DockerDocker 入门教程构建缓存镜像分层dockerignore层优化BuildKit

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 秒,这就是缓存的威力。

缓存什么时候失效

哪些改动会让缓存失效?记住三条:

  1. 指令本身变了。 比如 RUN apt-get install -y curl 改成 RUN apt-get install -y curl git,这条 RUN 的缓存就废了。
  2. 被拷贝的文件变了。 COPY . . 拷的文件里只要有任何一个内容或权限变了,这层缓存就失效。
  3. 前面的层失效了。 缓存是链式的,一旦某层失效,它后面的所有层都跟着失效,哪怕后面的指令没动。

第三条最关键,也是新手最容易踩的坑。看这个例子:

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.jsonrequirements.txtgo.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 有两个直接好处:

  1. 构建上下文变小,传给守护进程更快。
  2. 缓存更稳定,避免无关文件变动导致 COPY 层缓存失效。
Warning

如果不忽略 node_modules,本地的 node_modules 会被拷进镜像,覆盖掉容器里 yarn install 装的依赖。本地是 Mac、容器是 Linux,二进制不兼容,跑起来就崩。这是 Node 项目最常见的玄学报错根源之一。

合并 RUN 层,减少镜像体积

每条 RUNCOPYADD 都会产生一个新层。层越多,镜像越大。

把多个相关操作合并到一个 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 流水线里常用来保证构建的纯净,但日常开发用它会慢很多,别习惯性加。

如果只想重新执行某一层,可以在那条指令前做个小改动(比如改个注释),让它失效即可,不用全盘重来。

小结:构建优化的几条原则

把这一章浓缩成几条可操作的原则:

  1. 指令顺序按「变化频率」排:少变的(依赖清单)在前,多变的(源码)在后。
  2. 善用 .dockerignore:排除 node_modules.git、日志、构建产物。
  3. 合并 RUN:相关操作用 && 串起来,并在同一层里清理缓存。
  4. 钉死基础镜像版本:用 python:3.13.1 而不是 python:latest,保证构建可复现。
  5. COPY 优于 ADD:行为简单可控。
Note

Docker Engine 29.x 默认启用 BuildKit 构建器,缓存机制比老版本更智能,还会并行构建没有依赖关系的层。你基本不需要手动开启,但如果你看到构建日志是 [+] Building 这种树状输出,就说明 BuildKit 已经在工作了。

下一章我们讲多阶段构建—怎么把动辄几百 MB 的镜像,精简到几十 MB。