想象你在一家外卖中央厨房工作。顾客只点了一份三明治,但传统做法是:把整个备餐间——灶台、菜刀、一袋袋食材、用过的油、甚至垃圾桶——连同一只保温盒一起塞进外卖箱。骑车的人越走越沉,顾客还嫌盒子难闻、不卫生。Docker 里的单阶段构建就是这个味道:一个 Go 程序明明只有 8MB,镜像却常常超过 500MB,里面躺着编译器、依赖缓存、临时文件和包管理日志。 多阶段构建则像一个纪律严明的后厨:备餐间可以随便大,但出餐窗只递出一只整齐的保温盒,里面放做好的菜。于是镜像又小又干净,拉取快、启动快、攻击面小。这篇文章会带你把多阶段构建的原理、builder 模式、基础镜像选择、.dockerignore、缓存优化、安全扫描和常见踩坑一次讲清。

岗位关联

掌握多阶段构建,能显著降低镜像体积与交付风险,是部署 AI 服务和微服务时绕不开的工程基本功,也是 CI/CD 工程师观察力的重要分水岭。

目录
  1. 可视化演示
  2. 镜像膨胀根源
  3. 多阶段构建原理
  4. builder模式
  5. 基础镜像优化
  6. 上下文瘦身
  7. 构建缓存优化
  8. 镜像安全扫描
  9. 实践与踩坑
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示Docker多阶段构建流程、镜像体积对比及构建缓存优化策略,直观理解如何构建又小又干净的生产镜像。

本节配有交互动效(进入视口后自动加载,约 3KB)。

多阶段构建流程 Builder阶段编译源码产出二进制(585M) 仅复制二进制到Runtime阶段(27M) Final = Runtime(base + bin) 多阶段构建只保留运行所需文件丢弃编译依赖

镜像膨胀根源

回到中央厨房。一份三明治只需要面包、生菜、鸡胸肉,但你打开外卖箱,发现里面还塞着整套刀具、平底锅、半袋土豆、三桶酱料和昨天的厨余垃圾。这正是传统单阶段 Dockerfile 的日常:为了让一个很小的服务跑起来,以 ubuntu 为基础镜像,apt-get install 装编译器,npm install 拉全部依赖,最后连源码和缓存一起塞进镜像。

为什么这些杂物甩不掉?因为 Docker 镜像不是一块硬盘快照,而是一层一层叠加的只读层。每一条 RUN、COPY、ADD 都会生成一个新层,文件被添加到新层里。你在后面的层里执行 rm -rf,旧层中的文件不会真正消失,只是被新层标记为“删除”;只要有人提取旧层,文件立刻能回来。所以“装完再删”这条路被 Docker 层模型堵死了。

所以镜像膨胀的本质,是把备餐间的一切都装进了保温盒。要想外卖箱又轻又干净,不是往盒子里补一层“垃圾盖”,而是干脆不把厨房带出去——这正是下一节多阶段构建要解决的事。

image_size=base_image+∑i=1nΔlayeri\displaystyle \text{image\_size} = \text{base\_image} + \sum_{i=1}^{n} \Delta \text{layer}_i
镜像总大小等于基础镜像加上所有新增层的累加,且删除动作并不能让旧层消失。
  1. 基础镜像 base_image镜像的起点,例如 ubuntu 自带约 70MB 的系统和常用库。
    base_image\text{base\_image}
  2. 求和符号 \sum把每一条会生成层的指令都累加进来。
    ∑i=1n\sum_{i=1}^{n}
  3. 单层增量 \Delta layer_i每条 COPY/RUN 增加的体积,删除动作也只会产生正增量。
    Δlayeri=new_files+metadata\Delta \text{layer}_i = \text{new\_files} + \text{metadata}

多阶段构建原理

中央厨房做事讲分工:备餐间负责切、煮、炖,出餐台只管把成品装进保温盒。多阶段构建用多个 FROM 模拟这个分工。第一个 FROM 是备餐间,第二个 FROM 是出餐台,两者之间只隔一个小窗口。

窗口的学名是 COPY --from=阶段名。只要在某个阶段用 AS builder 命名,后续阶段就能从那个阶段里精确拷贝文件。最终镜像由最后一个阶段决定,前面阶段即使装了几百兆工具,也不会被“带出厨房”。

上面的例子只有两个阶段:builder 阶段用 Go 工具链编译出 server;最终阶段从 scratch 空镜像开始,只做一次 COPY,把 server 放进来。整个保温盒里只有一个可执行文件。

可以看到,多阶段构建不是“删掉不需要的层”,而是“从未把不需要的层装进最终镜像”。备餐间还在仓库里,但外卖箱里只有三明治。

final_image=scratch+∑j=1kCOPY(aj)\displaystyle \text{final\_image} = \text{scratch} + \sum_{j=1}^{k} \text{COPY}(a_j)
最终镜像由空白基础镜像与拷贝进来的产物构成,其余构建层全部被排除在门外。
  1. 最终阶段起点 scratchFROM scratch 让最终镜像从零开始,不带任何工具。
    scratch\text{scratch}
  2. 产物集合 a_j需要从各构建阶段拷贝的文件集合。
    ∑j=1kaj\sum_{j=1}^{k} a_j
  3. 拷贝操作 COPY(a_j)COPY --from 把产物放进最终镜像,等价于只装便当。
    COPY(aj)\text{COPY}(a_j)
dockerfile8 行
FROM golang:1.21 AS builderWORKDIR /appCOPY . .RUN go build -o server . FROM scratchCOPY --from=builder /app/server /serverENTRYPOINT ["/server"]

builder模式

说回中央厨房,你会专门给招牌菜配一间“备餐间”:里面有商用烤箱、搅拌机、喷枪,工人可以放飞自我。这就是 builder 模式的名字来源——第一个阶段被命名为 builder,专门负责构建产物。

builder 模式的思想是:构建阶段想用多大镜像都行,要能编译、能跑测试、能生成二进制;运行阶段则回到最简单的基础镜像,只保留可执行文件和必要的运行库。对于 Go、Rust 这类静态编译语言,最终阶段可以完全没有 shell、包管理器、编译器。

如果程序是 C 写的,依赖了动态库,别怕:在构建阶段用 ldd 列出 .so 文件,再在 COPY --from 中把这些库一并放到 lib 目录。这一步相当于把酱料、配菜单独装进便当盒的小格子,而不是连整个酱料桶搬上车。

builder 模式是使用最广的多阶段构建套路:备餐间再乱,出餐盒永远干净。

final_image=minimal_base+∑a∈artifactsCOPY(a)\displaystyle \text{final\_image} = \text{minimal\_base} + \sum_{a \in \text{artifacts}} \text{COPY}(a)
最终镜像等于“极小运行环境”加上“必要产物”,构建工具链被隔离在 builder 阶段。
  1. 极小基底 minimal_base运行阶段的基础镜像,不含包管理器与编译器。
    minimal_base\text{minimal\_base}
  2. 产物集合 artifacts用 ldd、项目打包配置等列出的必要文件集合。
    a∈artifactsa \in \text{artifacts}
  3. 逐个拷贝只把产物和必要的动态库放入最终镜像。
    COPY(a)\text{COPY}(a)

基础镜像优化

给外卖选盒子:有人用厚重的不锈钢饭盒,有人用轻便纸盒,还有人用一片荷叶把饭团一包。Docker 里也有这样的选择——基础镜像就是你的“盒子”。

scratch 是真正的空镜像,里面连 /bin/sh 都没有,体积为 0,适合静态二进制;alpine 基于 musl libc,完整基础镜像约 5MB,适合大多数可静态链接或 musl 兼容的程序;distroless 是 Google 提供的一组镜像,包含 Java、Python 等运行时,但没有 shell 和包管理器,安全和体积兼顾。选盒原则:够用就好,不要贪大。

选对盒子,重量从“一箱厨具”降到“一片荷叶”。多阶段构建的最后一步,常常就是在这些轻量基础镜像上完成的。

image_size≈base_size+artifact_size,base_size→0\displaystyle \text{image\_size} \approx \text{base\_size} + \text{artifact\_size}, \quad \text{base\_size} \to 0
镜像体积近似等于基础镜像加产物体积;当基础镜像趋近于 0 时,最终镜像就几乎等于程序本身。
  1. 近似符号 \approx暂不考虑文件系统元数据和压缩率差异。
    ≈\approx
  2. 基础镜像体积 base_size选得越小,留给杂物的空间越少。
    base_size\text{base\_size}
  3. 趋近于零 \to 0scratch 的 base_size 为 0,成品几乎就是全部。
    base_size→0\text{base\_size} \to 0

上下文瘦身

备餐间要开工,先把今天用不上的食材、旧菜单、废纸箱挡在后门外。.dockerignore 就是这个后门门卫。

docker build 启动时,Docker 客户端会把当前目录整个打成 context 发给 daemon。如果目录里有 .git、node_modules、dist、__pycache__、.env,它们都会坐进这趟“进货卡车”。不仅上传慢,一旦 Dockerfile 里写了 COPY . .,这些文件还会被原样装进备餐间,甚至进入最终镜像。

在项目根目录放一个 .dockerignore,用 .gitignore 的语法排除目录和文件。注意:它排除的是构建上下文,不是自动减基础镜像;如果显式 COPY 一个被忽略的文件,构建会直接报错——这反而是保护机制。

门卫挡掉的不是菜,而是所有不该进后厨的东西。少传一个 .git,可能比后续所有瘦身技巧更立竿见影。

context_size=∑f∈F∖I∣f∣\displaystyle \text{context\_size} = \sum_{f \in \mathcal{F} \setminus \mathcal{I}} |f|
构建上下文大小等于“全部文件”减去“被 ignore 排除的文件”后,剩余文件的大小之和。
  1. 全部文件 \mathcal{F}构建上下文根目录下所有文件集合。
    F\mathcal{F}
  2. 忽略文件 \mathcal{I}.dockerignore 规则匹配到的文件集合。
    I\mathcal{I}
  3. 差集 \setminus实际发送给 Docker daemon 的文件。
    F∖I\mathcal{F} \setminus \mathcal{I}

构建缓存优化

老汤店不会每天从洗锅开始熬高汤:只要汤底配方不变,昨天的老汤直接接着用。Docker 的层缓存,就像那锅老汤。

Docker 执行某条指令前,会检查指令文本和涉及的上下文文件是否与上次构建一致;如果一致,就直接复用缓存层,不再重新执行。为了喝到老汤,要把不常变的步骤放前面:先 COPY go.mod go.sum,再 RUN go mod download,最后 COPY . .。源码一改,前面的依赖层仍然命中。

如果反过来把 COPY . . 放最前面,任何源码变化都会让后面所有 RUN 全部重跑,缓存几乎全部失效。较新版本的 Docker 还支持 RUN --mount=type=cache,可以把 apt、pip 的下载目录挂到缓存卷,反复构建更快。

高汤前置、炉火不熄,便当出餐自然快。这也是为什么依赖与代码的顺序,常常比镜像体积数字更能影响工程师的幸福感。

Tbuild=∑i=1n(1−hi)⋅ti\displaystyle T_{\text{build}} = \sum_{i=1}^{n} (1 - h_i) \cdot t_i
构建总耗时等于所有“未命中缓存”的层所需时间之和;命中层可以直接跳过。
  1. 命中标记 h_i第 i 层是否命中缓存,命中为 1,未命中为 0。
    hi∈{0,1}h_i \in \{0,1\}
  2. 执行耗时 t_i第 i 层重新执行命令需要的时间。
    tit_i
  3. 只算未命中层1 - h_i 让命中层贡献为 0,未命中层贡献完整耗时。
    ∑i=1n(1−hi)⋅ti\sum_{i=1}^{n} (1 - h_i) \cdot t_i

镜像安全扫描

外卖盒封口前,质检员要过一道安检:食材有没有临期,包装有没有破损,盒子里有没有混入钢针。镜像安全扫描就是 Docker 世界的质检员。

扫描工具 Trivy、Grype 会把镜像每一层里的软件包、二进制文件与 CVE 漏洞库比对,给出严重等级。镜像越小、包越少,扫描面越小;这正是多阶段构建顺带带来的安全红利——没有编译器,就没有编译器漏洞;没有 shell,攻击者就没法轻易交互。

在 CI 中可以让扫描成为门禁:trivy image --exit-code 1 --severity HIGH --ignore-unfixed your-image,一旦发现高危漏洞就让流水线失败。还要切记:不要把数据库密码、API Key 写进 ENV 或 RUN 命令,镜像历史会留下痕迹。

安检通过,便当才会被递出窗外;扫不出高危,不只是“更安全”,也是让团队睡得着觉的硬指标。

severity=max⁡v∈vulns(image)score(v)\displaystyle \text{severity} = \max_{v \in \text{vulns}(\text{image})} \text{score}(v)
一个镜像的安全级别,取决于其中所有漏洞里最严重的那个,而不是平均分。
  1. 漏洞集合 vulns(image)从镜像层中识别出的所有已知漏洞。
    vulns(image)\text{vulns}(\text{image})
  2. 严重度 score(v)每个漏洞的 CVSS 分数或严重等级。
    score(v)\text{score}(v)
  3. 取最大值 \max只要有一个高危漏洞,整体风险就是高危。
    max⁡v∈vulns(image)score(v)\max_{v \in \text{vulns}(\text{image})} \text{score}(v)

实践与踩坑

会做便当不代表每次都能完美出餐。现实中,多阶段构建也有几个反复出现的翻车现场,就像把外卖小票和家里钥匙一起塞进了保温盒。

先讲应该怎么做:把最不容易变的 COPY 和 RUN 放前面;单条 RUN 里顺便清理 apt/pip 缓存;需要动态库时就 ldd 后精确拷贝 .so;所有阶段都用 AS 起清晰的名字,避免搞混。

再讲常踩的坑:一是以为 rm -rf /var/cache 能减镜像,其实旧层还在;二是最终阶段仍用 ubuntu 这种大镜像,前功尽弃;三是把密钥写进 ENV,任何人都能拉取镜像历史看到;四是阶段名拼错或漏掉 --from,构建直接失败。多阶段构建不是银弹,它是帮你把“必要文件、工具、临时文件、密钥”四者分开的手术刀。

一次成功的出餐,是把对的菜装进对的盒子,再通过对的窗口递出去。愿你下一次 docker build,也能收获一只轻巧、干净、安心的保温盒。

clean_image=necessary_files∖(tools∪temp∪secrets)\displaystyle \text{clean\_image} = \text{necessary\_files} \setminus (\text{tools} \cup \text{temp} \cup \text{secrets})
“干净镜像”等于运行程序真正需要的文件,减去构建工具、临时文件和机密信息。
  1. 必要文件 necessary_files程序运行期真正需要的二进制、配置和运行库。
    necessary_files\text{necessary\_files}
  2. 三类垃圾构建工具、临时文件、密钥合在一起。
    tools∪temp∪secrets\text{tools} \cup \text{temp} \cup \text{secrets}
  3. 做减法 \setminus从必要文件中剔除垃圾,才是最终镜像。
    ∖\setminus

AI 应用场景

模型服务镜像从 6GB 砍到 800MB

阶段:模型部署与上线

解决的问题:AI 模型服务通常是 Python + CUDA + 一堆 pip 包,单阶段构建会把编译缓存、wheel 包、源码和训练脚本一并打进镜像,动辄几个 GB。多阶段构建先在 builder 阶段安装依赖并编译扩展,最终阶段装 runtime 与模型文件,只保留服务依赖。

具体案例:某算法团队用多阶段构建部署 PyTorch 推理服务,把镜像从 6.2GB 减到 800MB,内网机器拉取时间从 5 分钟降到 40 秒,安全扫描的高危漏洞也少了一半。

CI 流水线构建与扫描门禁

阶段:持续集成/持续交付

解决的问题:项目组每个人各自构建镜像,标准不统一,镜像忽大忽小,漏洞没人管。用 Dockerfile lint + 多阶段构建 + Trivy 扫描作为 CI 门禁,保证每次提交产出的镜像都是最优且可追溯的。

具体案例:GitHub Actions 中 docker buildx build --push,随后 trivy image --exit-code 1 --severity HIGH,如果发现高危漏洞就阻止合并请求。持续两个月后镜像平均体积下降 70%。

边缘设备上的轻量模型网关

阶段:边缘推理

解决的问题:树莓派/工控机存储只有十几个 GB,镜像如果很大,更新一次就吃满磁盘。多阶段构建把 Go 写的模型网关编译成静态二进制,放进 scratch 镜像,整个镜像只有 10MB 左右。

具体案例:一个边缘项目用 FROM golang:1.21 AS builder 编译网关,再用 FROM scratch 拷贝 gateway 和 .so 模型文件,设备通过 OTA 更新镜像,带宽成本降低了 95%。

Python 演示

python45 行
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""Simulate image size for single-stage vs multi-stage Dockerfile."""  class Layer:    def __init__(self, label, delta_mb):        self.label = label        self.delta_mb = delta_mb  def single_stage(base_mb, layers):    total = base_mb    for layer in layers:        total += layer.delta_mb    return total  def multi_stage(runtime_base_mb, artifact_mb, build_layers):    temp_size = runtime_base_mb + sum(l.delta_mb for l in build_layers)    # temp_size is not part of the final image    return artifact_mb  BUILD_LAYERS = [    Layer("COPY source", 20),    Layer("apt-get install gcc", 420),    Layer("go mod download", 310),    Layer("go build", 16),]RUNTIME_BASE_MB = 0  # scratchARTIFACT_MB = 9  def main():    single = single_stage(72, BUILD_LAYERS)    multi = multi_stage(RUNTIME_BASE_MB, ARTIFACT_MB, BUILD_LAYERS)    print("single-stage image: {} MB".format(single))    print("multi-stage image: {} MB".format(multi))    saved = (1 - multi / single) * 100    print("size saved: {:.1f}%".format(saved))  if __name__ == "__main__":    main()
目的:用一个小脚本模拟 Dockerfile 的单阶段与多阶段构建,直观对比最终镜像体积。
  1. class Layer把 Dockerfile 中每一条会生成镜像层的指令建模为一个对象。
  2. def single_stage单阶段构建把所有层体积依次累加,对应“把厨房也装进外卖箱”。
  3. def multi_stage多阶段构建只返回运行基础镜像与产物体积,构建阶段层被丢弃。
  4. BUILD_LAYERS用几条典型指令表示编译器、依赖下载和编译产物。
  5. RUNTIME_BASE_MB = 00 表示 scratch 空基础镜像。
  6. saved = (1 - multi / single) * 100计算多阶段相对单阶段节省的体积百分比。
  7. if __name__ == "__main__"保证脚本可以直接运行,也方便被其他模块导入。

常见误区

误区“我用多阶段构建之后,镜像变小了,那第一阶段的几百兆层是不是也自动被删了?本地磁盘怎么还会被占满?”

正解多阶段构建只决定“最终镜像由哪些层组成”,不会自动删除已经产生的构建缓存。第一阶段层会留在本机 Docker 的缓存目录中,直到你执行 docker builder prune 或 docker system prune。就像备餐间虽不出菜,但厨房还在那里占着地方。

为何常见:人们把“最终镜像”和“构建环境”混为一谈,以为镜像变小等于所有现场都被清理了。

误区“安装完依赖后,用 `rm -rf /var/lib/apt/lists/*` 删除缓存,镜像应该会变小吧?”

正解Docker 的镜像是分层叠加,删除操作只是新写入一层“已删除”的标记,旧层中的文件仍然存在于镜像中。真正的瘦身靠两层:一是不装不需要的东西,二是用多阶段构建不把构建层拷贝进最终镜像。前者像做了也不加分,后者像根本没把东西放进外卖箱。

为何常见:因为普通文件和 Docker 层的删除语义不同,很多人照搬 Linux 习惯。

误区“scratch 是零体积镜像,我把 Python、Java 或 C 程序复制进去就能跑。”

正解scratch 没有 libc、没有 Python/JVM 运行库。Go/Rust 静态编译的程序可以跑;C 程序需要把动态库也拷进去;Python/Java 需要相应的运行时,通常更适合 distroless 或自建 runtime 基础镜像。就像荷叶包饭适合干爽的饭团,不适合汤汁四溢的红烧肉,除非额外加个托盘。

为何常见:被“scratch 零体积”宣传吸引,忽略了程序运行环境依赖。

误区“项目里加了 .dockerignore,密钥和 .env 就绝对进不了镜像。”

正解.dockerignore 只会让这些文件不进入构建上下文;如果 Dockerfile 里显式执行 COPY . .,它也会一起被过滤,但如果你写 COPY secrets.txt . 或把文件放在未忽略的目录且没被规则覆盖,敏感文件照样被复制。需要在 Dockerfile 里检查每一条 COPY/ADD,而不是只靠 .dockerignore 兜底。

为何常见:以为忽略规则等同于一把万能锁,其实它是门卫,不是保险柜。

误区“多阶段构建好,那我在 Dockerfile 里写十几个阶段,是不是镜像会越小?”

正解最终镜像大小只取决于最后一个阶段复制了什么,阶段数量多不一定小,反而会让构建依赖关系混乱、缓存更难维护。最佳实践是阶段要少而清晰:一个 builder 负责编译,必要时一个 intermediate 阶段负责整合产物,一个 runtime 阶段负责运行。多不一定干净,精准才叫干净。

为何常见:把“多阶段”误理解为“多用几段”,忽略了它的本质是产物隔离。

面试题

Q1. 多阶段构建最主要的好处是什么?

单选题 · easy

  • A.可以让容器运行速度更快
  • B.可以减小最终镜像体积← 正确答案
  • C.必须增加构建阶段数量
  • D.无需再指定基础镜像

答案:B。多阶段构建的核心是只把产物带进最终镜像,从而减小体积;容器运行速度不一定直接提升,阶段数量增加也不是目的,更不能免除基础镜像。好比外卖箱变小是因为只装便当,而不是因为骑手换了电动车。

Q2. FROM scratch 的含义是什么?

单选题 · easy

  • A.使用一个空的基础镜像← 正确答案
  • B.清空当前已有容器
  • C.跳过所有构建步骤
  • D.自动清理磁盘缓存

答案:A。scratch 是 Docker 提供的空镜像,里面没有任何文件和 shell。它不代表清空已有容器,也不等于跳过构建或清理磁盘。就像一片荷叶包饭,荷叶本身不提供任何配菜,只提供一个“载体”。

Q3. 多阶段构建中,从 builder 阶段拷贝产物到最终阶段,应该用哪个指令?

单选题 · mid

  • A.COPY --from=builder /app/server /server← 正确答案
  • B.ADD --stage=builder /app/server /server
  • C.RUN --from=builder cp /app/server
  • D.FROM builder COPY /app/server

答案:A。COPY --from=阶段名 是跨阶段拷贝产物的标准写法。ADD --stage、RUN --from=cp 都不是有效语法,FROM builder 是重新指定基础阶段而不是拷贝文件。这就像出餐窗口只认“便当传递单”,而不是把整个后厨搬过来。

Q4. 下列哪些做法能有效减小最终镜像体积?

多选题 · mid

  • A.使用 scratch 作为运行基础镜像← 正确答案
  • B.把依赖安装命令放在代码复制之前
  • C.把安装过程拆成多个 RUN 指令
  • D.只把构建产物复制到运行阶段← 正确答案

答案:AD。scratch 和“只复制产物”都是多阶段构建瘦身的直接手段;把依赖安装放在代码复制之前主要优化缓存,拆分 RUN 反而可能增加层数。前者像换小饭盒,后者像调整做菜顺序,只有前两者直接决定盒子里装什么。

Q5. Docker 构建上下文的大小取决于什么?

单选题 · mid

  • A.运行容器时挂载卷的大小
  • B.传给守护进程的文件总量← 正确答案
  • C.基础镜像的压缩层数量
  • D.最终镜像 COPY 的文件数

答案:B。构建上下文是 Docker CLI 发送给 daemon 的目录压缩包,大小取决于实际发送的文件总量。挂载卷、基础镜像层数和最终 COPY 文件数都不会直接决定上下文体积。就像进货卡车装多少菜,取决于你往卡车上放了多少菜,而不是厨房里有多少锅。

Q6. .dockerignore 文件的主要作用有哪些?

多选题 · mid

  • A.减小构建上下文传输体积← 正确答案
  • B.避免敏感文件被 COPY 进镜像← 正确答案
  • C.自动压缩基础镜像的体积
  • D.让所有 RUN 指令并行执行

答案:AB。.dockerignore 可以排除文件和目录,从而减小发送给 daemon 的体积,也能避免这些文件被 COPY . . 带进镜像。它不能压缩基础镜像,也不能并行执行 RUN 指令。它像一个门卫,只决定谁不能进后厨,不改菜谱,也不加速做菜。

Q7. 传统 Dockerfile 中执行 RUN rm -rf /var/cache 后,为什么镜像体积没有变小?

单选题 · hard

  • A.删除行为不会被记录到层
  • B.旧层仍在,删除只是新增层← 正确答案
  • C.镜像层只增不减不能读取
  • D.Docker 会直接忽略 rm 指令

答案:B。Docker 的层是只读叠加,删除文件只是在新层写入“已删除”标记,旧层文件依然可以被提取。所以镜像不会变小,反而多了一层。好比你在外卖盒上贴了一张“已倒掉垃圾”的标签,但垃圾还在盒子底部。

Q8. CI 中使用 trivy image --exit-code 1 --severity HIGH 的主要作用是什么?

单选题 · hard

  • A.扫描到高危漏洞时让流水线失败← 正确答案
  • B.自动修复所有高危镜像漏洞
  • C.把镜像推送到仓库并打上标签
  • D.直接删除镜像中的高危文件

答案:A。--exit-code 1 表示发现对应严重等级的漏洞时返回非 0 退出码,从而阻止 CI 继续。它不会修复漏洞、推送镜像或删除文件。这就像安检发现管制刀具后拒绝放行,而不是帮你把刀磨得更快。

Q9. 关于多阶段构建缓存优化,下列说法正确的是?

多选题 · hard

  • A.不常变的依赖安装应放在 Dockerfile 前部← 正确答案
  • B.先 COPY 依赖文件再 RUN 下载更容易命中缓存← 正确答案
  • C.使用挂载缓存可缓存包管理器下载文件← 正确答案
  • D.只要基础镜像相同后续所有层必然命中缓存

答案:ABC。把不常变的依赖层前置,可以让源码变化时依赖层依然命中缓存;RUN --mount=type=cache 能缓存包下载。D 错在“必然”,只要指令文本或上下文文件变了,缓存就会失效。就像老汤配方一变,锅就得重洗重熬。

Q10. 多阶段构建后,第一阶段镜像不在最终镜像中,但仍占用磁盘空间。下列哪项能主动回收这部分空间?

单选题 · extreme

  • A.重新执行 docker build 后自动回收
  • B.执行 docker builder prune 清理缓存← 正确答案
  • C.修改 Dockerfile 阶段名后自动清理
  • D.删除最终镜像即可连带清理缓存

答案:B。第一阶段层会保留在本地 build cache 中,需要 docker builder prune 这类命令主动清理。重新 build、改阶段名、删除最终镜像都不会自动回收 build cache。就像备餐间的旧灶台还堆在仓库里,不叫人来搬走,它不会自己消失。