层、缓存与小镜像 · DevOps:容器、CI 与 Kubernetes
为什么有的 Docker 构建几秒钟,有的要好几分钟?
课程目录为什么需要它
你一天会重建很多次镜像,CI 也一样。顺序写得好的 Dockerfile 几乎全部复用,几秒钟就完成;顺序写得不好的,每改一点代码就要重装所有库。小镜像下载、启动、打补丁也更快,里面能被攻击者利用的工具也更少。
直觉理解
每条指令都会生成一个层,Docker 会把它们缓存起来。下次构建时,只要某一步依赖的东西没变,就直接复用。
关键规则:第一个文件有变化的 COPY 会让缓存失效,这一步以及之后的每一步都要重新运行。
所以 Dockerfile 要按「变化最少」到「变化最多」的顺序写:
- 基础镜像和系统软件包
- 依赖清单(
requirements.txt、package.json)和安装步骤 - 你的代码,它一直在变
这样改了 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"]- L3复制所有东西,包括一直在变的 app.py。
- 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"]- L3requirements.txt 很少变,所以这一步通常是缓存的。
- L4只要 requirements.txt 没变,这一步也是缓存的。
- 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"]- L1第 1 阶段,名叫 build:带着完整的 Go 工具链。
- L8第 2 阶段从一个几乎什么都没有的镜像开始:没有 shell,没有包管理器。
- 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。哪些步骤要重新运行?
查看答案
答案:A
第 3 步的输入变了,所以它和之后的所有步骤都要重新运行。
2. `COPY --from=build /out/app /app` 做了什么?
查看答案
答案:A
--from 指向同一个 Dockerfile 里的另一个阶段。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出哪些步骤要重建?
你改了
src/server.js,然后重新构建。哪些步骤要重新运行? - 改需求别再重装了
每次改
app.py都会重装所有库。调整 Dockerfile 的顺序,让修改app.py时不会重新运行任何RUN步骤,而修改requirements.txt时仍然会。 - 从零写小巧的 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。
- 第 1 阶段基于
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| 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 | .dockerignore | Files Docker should leave out of COPY . . |