人人都能学编程
English 开始学习
专项 · DevOps第 2 课 / 共 5 课免费阅读

层、缓存与小镜像 · DevOps:容器、CI 与 Kubernetes

为什么有的 Docker 构建几秒钟,有的要好几分钟?

课程目录

为什么需要它

你一天会重建很多次镜像,CI 也一样。顺序写得好的 Dockerfile 几乎全部复用,几秒钟就完成;顺序写得不好的,每改一点代码就要重装所有库。小镜像下载、启动、打补丁也更快,里面能被攻击者利用的工具也更少。

直觉理解

每条指令都会生成一个层,Docker 会把它们缓存起来。下次构建时,只要某一步依赖的东西没变,就直接复用。

关键规则:第一个文件有变化的 COPY 会让缓存失效,这一步以及之后的每一步都要重新运行。

所以 Dockerfile 要按「变化最少」到「变化最多」的顺序写:

  1. 基础镜像和系统软件包
  2. 依赖清单(requirements.txt、package.json)和安装步骤
  3. 你的代码,它一直在变

这样改了 app.py,只会重新运行最后的 COPY . .,而不是慢吞吞的安装步骤。

多阶段构建让最终镜像保持小巧:在带编译器的大镜像里构建,然后只把结果复制到一个很小的镜像里:

FROM golang:1.22 AS build
…
FROM gcr.io/distroless/static
COPY --from=build /out/app /app

加一个 .dockerignore 文件,让 COPY . . 不会把 .git、node_modules 或密钥带进镜像。

定义

层

The result of one Dockerfile instruction. Images are stacks of layers, cached and shared between builds.
一条 Dockerfile 指令的结果。镜像就是一层层叠起来的,在构建之间缓存和共享。

构建缓存

Docker reuses a step if nothing it depends on changed; the first changed COPY invalidates it and all later steps.
某一步依赖的东西没变,Docker 就复用它;第一个有变化的 COPY 会让它和之后所有步骤失效。

多阶段构建

A Dockerfile with several FROM lines: build in one stage, copy only the result into a small final stage.
有多个 FROM 的 Dockerfile:在一个阶段里构建,只把结果复制到小巧的最终阶段。

示例

任何代码改动都会让第 3 步的 COPY 失效,所以每次都要重新 pip install。

FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
USER 1000
CMD ["python", "app.py"]
逐步执行
  1. L3复制所有东西,包括一直在变的 app.py。
  2. L4排在缓存失效之后,所以每改一次代码就要重装所有库。

依赖在前:改 app.py 只会重新运行第 5 步。

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER 1000
CMD ["python", "app.py"]
逐步执行
  1. L3requirements.txt 很少变,所以这一步通常是缓存的。
  2. L4只要 requirements.txt 没变,这一步也是缓存的。
  3. L5你的代码:编辑之后唯一要重新运行的步骤(加上它后面那些很快的步骤)。

Go 的多阶段构建

在互动课里运行

在大的 Go 镜像里编译;只交付二进制文件。

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app .

FROM gcr.io/distroless/static
COPY --from=build /out/app /app
USER 65532
CMD ["/app"]
逐步执行
  1. L1第 1 阶段,名叫 build:带着完整的 Go 工具链。
  2. L8第 2 阶段从一个几乎什么都没有的镜像开始:没有 shell,没有包管理器。
  3. L9只从第 1 阶段拿编译好的程序。编译器留在后面。

常见错误

安装之前就 COPY . .

这样每次改代码都会重装全部依赖。先复制依赖清单,安装,再复制代码。

没有 .dockerignore

COPY . . 会把目录里的一切都带上:.git、本地虚拟环境、带密钥的 .env 文件。把它们写进 .dockerignore。

把构建工具也交付出去

最终镜像里的编译器和包缓存会让它更大,也给攻击者更多可用的东西。用多阶段构建。

小测验

1. 步骤:1 FROM,2 WORKDIR,3 COPY requirements.txt,4 RUN pip install,5 COPY . .,6 CMD。你改了 requirements.txt。哪些步骤要重新运行?

  1. A3、4、5 和 6
  2. B只有 4
  3. C只有 3
查看答案

答案:A

第 3 步的输入变了,所以它和之后的所有步骤都要重新运行。

2. `COPY --from=build /out/app /app` 做了什么?

  1. A从前面名叫 build 的阶段复制文件,而不是从你的项目复制
  2. B从一个叫 build 的网站下载文件
  3. C重新构建应用
查看答案

答案:A

--from 指向同一个 Dockerfile 里的另一个阶段。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出哪些步骤要重建?

    你改了 src/server.js,然后重新构建。哪些步骤要重新运行?

  2. 改需求别再重装了

    每次改 app.py 都会重装所有库。调整 Dockerfile 的顺序,让修改 app.py 时不会重新运行任何 RUN 步骤,而修改 requirements.txt 时仍然会。

  3. 从零写小巧的 Go 镜像

    给一个 Go 程序写一个多阶段的 Dockerfile:

    • 第 1 阶段基于 golang:1.22,命名为 build:复制代码,运行 go build -o /out/app .
    • 最终阶段基于一个很小的镜像(gcr.io/distroless/static 或 alpine:3.20),只从 build 复制 /out/app
    • 用非 root 用户运行,用 ["/app"] 启动

    最终镜像的估计大小要小于 50 MB。

在互动课里练这一课

打开互动课,这些例子都能直接改、直接运行,练习题提交马上判题。免费开始,不用注册。

互动课按顺序解锁:前面的课还没学,会先带你到课程地图。

本课术语

术语中文英文定义
Layer镜像层The result of one Dockerfile instruction; cached between builds.
Build cache构建缓存Reused layers; the first changed COPY breaks it.
Multi-stage build多阶段构建Build in one stage, ship only the result from a small one.
.dockerignore.dockerignoreFiles Docker should leave out of COPY . .