目录

Docker 构建缓存教程:掌握缓存规则并精简镜像

同一个 Dockerfile,写法不同,构建时间可能从不到 1 秒变成好几分钟,镜像大小也可能相差很多。本节通过实验说明缓存规则和精简技巧。

缓存如何工作

docker build 会依次处理每条指令。对于每一行,Docker 都会检查:这条指令以及它之前的所有层,是否与上次构建时相同?

  • 相同:直接使用缓存,输出会显示 CACHED。
  • 不同:重新执行这一行,而且之后的每一行都不再使用缓存。
指令判断依据
RUN指令字符串是否相同(不会检查网络上的软件包是否已更新)
COPY / ADD被复制的文件内容是否相同
其他指令和参数是否相同
修改代码后重新构建:先安装软件包的顺序会让 npm ci 使用缓存,先复制源代码的顺序则会让 npm ci 重新执行
一层发生变化后,它下面的每一层都要重新构建:把不常变化的步骤放在前面

实验:指令顺序的影响

使用一个依赖 Express 的小项目(package.json 中有 express 包),比较两种 Dockerfile。

错误的顺序(Dockerfile.bad):

FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

正确的顺序(Dockerfile.good):

FROM node:24-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]

错误的顺序:修改一行代码

先构建一次,然后只修改 server.js 中的一个字,再次构建:

$ docker build -f Dockerfile.bad -t cache-bad .

[+] Building 6.9s (9/9) FINISHED                                                     docker:default
 => [internal] load build definition from Dockerfile.bad                                       0.0s
 => => transferring dockerfile: 125B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.2s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/4] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 280B                                                              0.0s
 => CACHED [2/4] WORKDIR /app                                                                  0.0s
 => [3/4] COPY . .                                                                             0.1s
 => [4/4] RUN npm install                                                                      5.4s
 => exporting to image                                                                         1.0s
 => => exporting layers                                                                        0.5s
 => => exporting manifest sha256:d27475f082a53018e303541bc1c0691ac25526e87761300e2aa8ec76d73e  0.0s
 => => exporting config sha256:19f2024df4329a5f2af23e50d8c18826f073d425e18133e9571c393999d913  0.0s
 => => exporting attestation manifest sha256:58b4730e9566f2ec7e8222cd41c14ea971d87272b6b0c190  0.0s
 => => exporting manifest list sha256:58b8fc79124f2fe21270801d5e40f09839c06ba5203304423125270  0.0s
 => => naming to docker.io/library/cache-bad:latest                                            0.0s
 => => unpacking to docker.io/library/cache-bad:latest                                         0.4s

[3/4] COPY . . 因为 server.js 发生变化而重新执行,后面的 [4/4] RUN npm install 也随之重跑,花了 5.4 秒重新下载并安装所有包。

正确的顺序:修改一行代码

第一次构建(需要执行 npm install):

$ docker build -f Dockerfile.good -t cache-good .

[+] Building 5.7s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile.good                                      0.0s
 => => transferring dockerfile: 147B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.1s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 95B                                                               0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => [3/5] COPY package.json ./                                                                 0.0s
 => [4/5] RUN npm install                                                                      4.4s
 => [5/5] COPY . .                                                                             0.1s
 => exporting to image                                                                         0.9s
 => => exporting layers                                                                        0.5s
 => => exporting manifest sha256:4b36d92a774b02cc1a188193e178a445b90f330b19f2dcc741d631e246e8  0.0s
 => => exporting config sha256:4a597aa667d9ba52eab36c4483f7507bbcae158b1b22af5ca6e4ac5b7020fa  0.0s
 => => exporting attestation manifest sha256:efea2ef65e2672718391b6419b9f5f2be50396163032a29e  0.0s
 => => exporting manifest list sha256:236918100fd2092d0388da5c03b32dfa3abaa2e62304abb3e3dd335  0.0s
 => => naming to docker.io/library/cache-good:latest                                           0.0s
 => => unpacking to docker.io/library/cache-good:latest                                        0.4s

修改 server.js 后再次构建:

$ docker build -f Dockerfile.good -t cache-good .

[+] Building 0.4s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile.good                                      0.0s
 => => transferring dockerfile: 147B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.1s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 280B                                                              0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => CACHED [3/5] COPY package.json ./                                                          0.0s
 => CACHED [4/5] RUN npm install                                                               0.0s
 => [5/5] COPY . .                                                                             0.0s
 => exporting to image                                                                         0.1s
 => => exporting layers                                                                        0.0s
 => => exporting manifest sha256:6cd8c2671a295aab85724aefe72f8dc749f4d332224f5d534fff5e4e57fa  0.0s
 => => exporting config sha256:7df3d574ae91f98ea3634c4dcede63e64ce8018c902643c2db08c2b4dee4c3  0.0s
 => => exporting attestation manifest sha256:fc9ff7e57f4187c04b74446f5dd96e46ef9c7f0ea88372a6  0.0s
 => => exporting manifest list sha256:79e8a6d76ff2382f02bcadc183a44670959d1d0a30467a27b9d619e  0.0s
 => => naming to docker.io/library/cache-good:latest                                           0.0s
 => => unpacking to docker.io/library/cache-good:latest                                        0.0s

整个构建只花了 0.4 秒:COPY package.json、RUN npm install 都显示 CACHED,只有最后的 COPY . . 重新执行。这个项目只有一个依赖;真实项目动辄包含几百个包,npm install 可能要好几分钟,差距会更大。

同样的道理适用于所有语言:

语言先复制再安装
Node.jspackage.json、package-lock.jsonnpm ci
Pythonrequirements.txt 或 pyproject.tomlpip install
Gogo.mod、go.sumgo mod download
Java (Maven)pom.xmlmvn dependency:go-offline
PHPcomposer.json、composer.lockcomposer install

实验:在同一层中清理临时文件

每一层都会叠加在前一层之上。在后面的 RUN 中删除文件,并不会减少前一层的大小。比较安装 curl 的两种写法:

Dockerfile.separate:

FROM debian:trixie-slim
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl
RUN rm -rf /var/lib/apt/lists/*

Dockerfile.same:

FROM debian:trixie-slim
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

构建后比较大小:

$ docker images --filter reference=apt-separate --filter reference=apt-same --filter reference=debian:trixie-slim
IMAGE                 ID             DISK USAGE   CONTENT SIZE   EXTRA
apt-same:latest       5357e9775c1e        136MB         34.9MB
apt-separate:latest   ad6c0a37d9ea        174MB         51.7MB

内容完全相同(都安装了 curl,也都删除了软件包列表),但 apt-separate 却大了 38 MB。用 docker history 查看原因:

$ docker history apt-separate
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
ad6c0a37d9ea   11 seconds ago   RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #…   20.5kB    buildkit.dockerfile.v0
<missing>      11 seconds ago   RUN /bin/sh -c apt-get install -y --no-insta…   13.4MB    buildkit.dockerfile.v0
<missing>      16 seconds ago   RUN /bin/sh -c apt-get update # buildkit        21.6MB    buildkit.dockerfile.v0
<missing>      8 days ago       # debian.sh --arch 'amd64' out/ 'trixie' '@1…   87.7MB    debuerreotype 0.17

$ docker history apt-same
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
5357e9775c1e   1 second ago   RUN /bin/sh -c apt-get update     && apt-get…   13.4MB    buildkit.dockerfile.v0
<missing>      8 days ago     # debian.sh --arch 'amd64' out/ 'trixie' '@1…   87.7MB    debuerreotype 0.17

apt-separate 的 apt-get update 层有 21.6 MB(下载的软件包列表),后面的 rm -rf 只是在新的一层中将其“标记为删除”,原来的 21.6 MB 仍留在镜像里。把命令写在同一个 RUN 中,软件包列表会在这一层结束前删除,从未存入任何一层。

各种语言常用的“不留临时文件”写法:

RUN pip install --no-cache-dir -r requirements.txt
RUN npm ci && npm cache clean --force
RUN apk add --no-cache curl

.dockerignore

构建开始时,Docker 会把整个构建上下文(通常是当前文件夹)发送给构建引擎。没有排除 node_modules 时,Dockerfile 入门中的示例需要传送 30 MB,排除后只有 159 B。.dockerignore 示例:

.git
node_modules
dist
*.log
.env
.env.*
Dockerfile*
.dockerignore

这样不仅能加快构建,也能避免这些文件的变化导致 COPY . . 缓存失效,并防止 .env、私钥被打包进镜像。

选择精简的基础镜像

镜像一节实测的 Python 3.14 镜像大小:

基础镜像硬盘空间
python:3.141.63 GB
python:3.14-slim180 MB
python:3.14-alpine82.6 MB

多阶段构建

编译工具和开发包只在构建时需要,不应留在最终镜像中。多阶段构建是最有效的精简方式。

缓存挂载:加快软件包下载

BuildKit 支持缓存挂载,让包管理工具的下载缓存可以在多次构建之间保留。即使 RUN npm ci 这一层的缓存失效,也不必从网络重新下载所有包:

# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

缓存挂载的内容不会进入镜像,因此不会增加镜像大小。

强制不使用缓存

需要获取软件包的最新版本,或怀疑缓存有问题时:

$ docker build --no-cache -f Dockerfile.good -t cache-good .

[+] Building 6.6s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile.good                                      0.0s
 => => transferring dockerfile: 147B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.3s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 280B                                                              0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => [3/5] COPY package.json ./                                                                 0.0s
 => [4/5] RUN npm install                                                                      5.1s
 => [5/5] COPY . .                                                                             0.1s
 => exporting to image                                                                         1.0s
 => => exporting layers                                                                        0.4s
 => => exporting manifest sha256:30688d5c22069d97e9db0bd32d5939e8e99fda0c6a0c4af1f32fb5baacc9  0.0s
 => => exporting config sha256:e8e75108925e8f145e7367b8e36350a8175c3776154f560fb47a9fb287fe21  0.0s
 => => exporting attestation manifest sha256:adbbb2fc6683c9a35c67bb9237cf3afcfd24c39dd9aa4b55  0.0s
 => => exporting manifest list sha256:6b92b5704d4fa7a3f11cfda5ffde00e6516bdbf358e23dbece712a2  0.0s
 => => naming to docker.io/library/cache-good:latest                                           0.0s
 => => unpacking to docker.io/library/cache-good:latest                                        0.5s

--no-cache 会让每一步都重新执行(没有任何 CACHED)。加上 --pull 还会重新检查基础镜像是否有新版本。

查看和清理构建缓存

构建缓存会不断累积:

$ docker builder du
ID                           RECLAIMABLE   SIZE       LAST ACCESSED
mr6xd3ylof28pr8obxiet3fo8    true          403B       3 minutes ago
l34l5occgcgh8bscivg2iusrn    true          626B       3 minutes ago
... (中间省略)
Shared:		488.2MB
Private:	118.5MB
Reclaimable:	606.7MB
Total:		606.7MB
$ docker builder prune -f
ID						RECLAIMABLE	SIZE		LAST ACCESSED
xqb9jtg0kmq0mdzb0vrmod68j               	true 		20.67kB   	3 minutes ago
u1pbndrekyh1q91qbb4n9e3fy*              	true 	8.192kB   	5 minutes ago
... (中间省略)
Total:	118.5MB

docker builder prune 只会删除构建缓存,不会影响镜像和容器。清理所有 Docker 数据的方法见清理硬盘空间。

重点回顾

  • 某一层发生变化后,它后面的所有层都要重新构建。
  • 把不常变化的步骤(安装软件包)放在前面,常变化的步骤(复制代码)放在后面。
  • 在同一个 RUN 中下载、安装并清理临时文件,临时文件才不会留在镜像中。
  • RUN --mount=type=cache 可以让软件包下载缓存供多次构建共用。

动手练习

练习 1:下面两个 Dockerfile 片段,哪一个生成的镜像更小?为什么?

# A
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# B
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
答案

B 更小。A 的 rm 在新的一层中执行,软件包列表仍留在前面的层中;B 在同一层中下载并删除,列表没有被保存下来。

练习 2:对同一个项目分别使用 docker build --no-cache 和不带参数的命令构建一次,比较 [+] Building 显示的总时间。

提示

不带参数时,大多数步骤会显示 CACHED,通常只需零点几秒;--no-cache 会重新执行每一步,耗时取决于安装软件包的速度。

常见问题

RUN apt-get update 被缓存,安装到旧版软件包怎么办?

缓存只比较指令字符串,不会检查网络上的软件包是否已更新。把 apt-get update 和 apt-get install 写在同一个 RUN 中;需要时使用 docker build --no-cache 强制重新构建。

构建缓存占用很多硬盘空间?

使用 docker builder du 查看,用 docker builder prune 清理;实测见[清理硬盘空间](/zh/docker/cleanup/)。

在 CI 中每次都从头构建很慢?

使用外部缓存,例如 GitHub Actions 的 cache-from: type=gha,见[GitHub Actions 自动构建](/zh/docker/ci-cd/)。

更新于 2026年10月14日