想象你刚买下一套新房。装修队进驻后,客厅堆满水泥袋、油漆桶、电钻、切割机,墙上还贴着施工图纸。终于装修完工了,你会把电钻留在茶几上吗?当然不会——你把成品家具搬进新家,装修工具通通撤走。Docker 镜像构建也是这个道理:编译一个 Go 程序需要完整的编译工具链,但程序跑起来只需要那个可执行文件。传统的单一阶段构建,就像把整个装修工地原封不动搬进新家,镜像动辄几百 MB;而多阶段构建,是让你在 Dockerfile 里写出多个“工棚”(构建阶段),每个阶段负责不同的工序,最后只把“成品”搬进真正入住的小房子。本文就用装修这条主线,讲透多阶段构建的原理、阶段命名、依赖管理、缓存策略、多语言实战、安全加固与性能调优。
多阶段构建是容器化镜像优化的核心技能。掌握它,你就能为 AI 推理服务交付更小、更快、更安全的镜像,提升部署效率,展现工程化的硬实力。
目录
可视化演示
对比单阶段与多阶段Docker镜像构建,展示分层缓存复用与攻击面缩减效果,含构建流程与安全指标可视化。
本节配有交互动效(进入视口后自动加载,约 3KB)。
单阶段构建:镜像臃肿 编译器/依赖/源码全部打包 镜像体积达850M+ size = base + deps + build + src 单阶段将构建工具和源码残留进最终镜像,体积臃肿且攻击面大。
基础原理——装修和入住,分开进行
你请装修队给新家施工,客厅里先堆满水泥袋、油漆桶、电钻和图纸。等装修完毕,你最想做的当然是请装修队带着工具撤场,只把漂亮的成品留下来。传统 Docker 单阶段构建恰恰相反:它在一个基础镜像里装好编译器、拉取源码、生成产物,整个“工地”原封不动成了你的“家”。这就是搬家时把所有工具和建材都搬进客厅的尴尬。
多阶段构建打破了这种“一个 Dockerfile 对应一个环境”的思维。你可以在同一个 Dockerfile 里写多个 FROM 指令,每个 FROM 开启一个独立的构建阶段。前面的阶段负责编译、打包、生成安装文件,可以看作装修工棚;最后一个阶段只负责“入住”——把产物抄过来,配上最小化的运行依赖。
最终镜像的体积可以用这样一个关系表示:
所以,多阶段构建不是魔法,它只是让“装修”和“入住”各归各位。装修队在工棚里干活,新家只接收成品,镜像自然轻薄。
- 最终镜像体积$V_{\text{final}}$ 表示多阶段构建后真正交付的镜像大小,它只包含“搬进新家”的部分。
- 产物加运行环境$V_{\text{artifact}} + V_{\text{runtime}}$ 是最终镜像的两块内容——编译出的可执行文件,以及运行它所需的最小系统库。
- 与单阶段镜像的关系$V_{\text{single}} - V_{\text{toolchain}} - V_{\text{source}}$ 说明单阶段镜像里有大量的工具链和源码是运行时根本用不到的,多阶段构建只是把它们从“新家”里请了出去。
# 阶段一:装修工棚(构建环境)FROM golang:1.21 AS builderWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o /app/server # 阶段二:正式入住(运行环境)FROM debian:12-slimWORKDIR /optCOPY --from=builder /app/server /opt/serverENTRYPOINT ["/opt/server"]
阶段命名与引用——给工棚挂上名牌
装修现场如果同时有三个工棚,你喊“把客厅吊灯递过来”,总比“东边第二个蓝顶棚里的纸箱”要靠谱得多。Docker 多阶段构建里的阶段也一样:每个 FROM 都可以用 AS 起一个语义化的名字,之后 COPY、构建指令想引用它时,只需叫名字。
这里 build 阶段先完成 Node 项目的依赖安装和打包,随后 server 阶段用 COPY --from=build 直接把构建产物(静态文件)抄进 nginx 的目录。整个流程可以浓缩成一条产物链:
如果你只想单查 builder 阶段的产物,不必构建完整镜像,用 docker build --target=builder -t debug . 就能精准停在某个工棚入口,像只验收装修到一半的房子。
给阶段起好名字,既让 Dockerfile 可读,也让后续维护者一眼看懂“哪件家具是从哪个工棚搬来的”。名字,就是多阶段构建的指路牌。
- 阶段起点$\text{FROM}\;golang$ 声明构建从 Go 基础镜像开始,这是工棚的地基。
- 语义命名$\xrightarrow{\text{AS}}$ 通过 AS 关键字把阶段命名为 builder,相当于给工棚挂上名牌。
- 产物搬运$\xrightarrow{\text{COPY --from=builder}}$ 最后从名为 builder 的工棚里取出成品,送进 final 运行环境。
FROM node:20 AS buildWORKDIR /appCOPY package.json ./RUN npm ciCOPY . .RUN npm run build FROM nginx:alpine AS serverCOPY --from=build /app/dist /usr/share/nginx/html
构建依赖管理——工具箱只留在工棚
装修时,瓦刀、电钻、切割机都是必需品;可入住后,你只需要毛巾、牙刷和锅碗瓢盆。构建依赖与运行依赖天然是不同的集合——编译一个 Python 扩展需要 GCC、Python 头文件;但运行编译好的 .so 只需要系统动态库和 Python 解释器。
多阶段构建最擅长做这种“工具箱隔离”。在 builder 阶段,你想装多少编译依赖都行,反正这些工具不会进入最终镜像;到了 final 阶段,只装运行所需的最小依赖。一个典型的 Python 项目:
数学上,构建期与运行期的依赖集合应该是互斥的:
ARG 与 ENV 是这段“施工管理”中的搭档:ARG 只在构建期生效,传参给 go build -ldflags "-X main.version=${VERSION}";ENV 则固化到最终镜像,供运行时读取。区分好它们,就像分清楚施工图纸和入住使用手册。
把工具箱留在工棚,新家自然干净。多阶段构建让你理直气壮地对编译器说:你的使命到工棚门口就结束了。
- 构建期依赖$D_{\text{build}}$ 是编译阶段需要的全部软件包集合,例如 GCC、make、Python 头文件,相当于装修工具。
- 运行期依赖$D_{\text{runtime}}$ 是程序跑起来需要的软件包集合,例如 glibc、libssl,相当于生活日用品。
- 交集为空$\emptyset$ 表示两个集合理论上不该有交集,工具不该留在生活区。多阶段构建正是用物理隔离保证了这个空集。
FROM python:3.12-slim AS builderRUN apt-get update && apt-get install -y build-essentialCOPY requirements.txt ./RUN pip wheel --wheel-dir=/wheels -r requirements.txt FROM python:3.12-slimCOPY --from=builder /wheels /wheelsRUN pip install --no-index --find-links=/wheels -r requirements.txt
缓存利用策略——趁手的工具先摆好
装修入场时,如果每次改动一面墙的图纸,就要把整个工具箱重新翻一遍、所有材料重新搬一次,那工期就注定了要崩溃。聪明的师傅会先把常用的工具和材料放在顺手的位置,再开始动会频繁修改的部分。Docker 镜像的层缓存机制正是如此。
Docker 的每条指令都会生成一层。某层内容没变,构建时就能直接从缓存复用;一旦某层变了,它之后的所有层都得重建。所以 Dockerfile 的指令顺序要遵循“先放不变的依赖,再放常改的源码”:
构建总耗时可以抽象为缓存命中层与未命中层耗时之和:
所以,把 COPY requirements.txt 和 RUN pip install 放在 COPY . . 之前,就是让“常驻工具箱”先就位;而把易变的源码放在最后,相当于只让“图纸”频繁替换。缓存策略的核心,是让变更局部化,别让一次小改动推倒整面墙。
- 总耗时$T$ 是执行一次 docker build 的全部时间,等于从缓存里取层的耗时加上真正重建层的耗时。
- 命中部分$\sum_{i \in \mathcal{H}} t_i$ 表示所有缓存命中层的时间累加,$\mathcal{H}$ 是命中层的集合——这些层像“拆开即用的成品工具”,秒级复用。
- 未命中部分$\sum_{j \in \mathcal{M}} t_j$ 表示需要重新构建的层,$\mathcal{M}$ 是未命中层的集合。一旦源码层变化,它和后续所有层都要从零开始。
FROM node:20-alpine AS buildWORKDIR /app# 依赖清单先拷贝并安装 —— 这层不常变COPY package.json package-lock.json ./RUN npm ci# 源码最后拷贝 —— 这层天天变COPY . .RUN npm run build
多语言实践——不同工种的工具箱
装修队里,木工用的锯子、水电工用的热熔机、油漆工的滚刷各不相同。Go、Python、Node 这三个主流语言,由于编译特性和依赖模型的差异,多阶段构建的“工具箱”也各自不同。
Go 是最省心的“成品家具”,能静态编译。在构建阶段设置 CGO_ENABLED=0,生成的二进制不依赖任何动态库,几乎可以放进“空房间”(scratch 基础镜像):
Python 则是“需要自带电力系统的精装房”——它必须带着解释器和一堆依赖。pipeline 的关键是把依赖集中打包:
Node 介于两者之间:构建产物是静态文件或打包好的 JS,最终镜像可以用 nginx 或轻量 node-alpine 承载,关键是构建阶段用 npm ci --only=production 避免安装开发依赖。
三种语言的镜像体积构成可以写成:
理解了各路工具的脾气,就能为每种语言定制最贴合的施工方案。多阶段构建不是一套模板,而是一套“按工种分工”的方法论。
- Go 的体积$V_{\text{Go}} \approx V_{\text{binary}}$ 指静态编译后镜像几乎就等于二进制本身的大小,没有运行环境负担。
- Python 的体积$V_{\text{Python}} = V_{\text{interpreter}} + V_{\text{deps}}$ 指它必须带上解释器(“发电机”)和第三方库(“电线”),所以体积总是有下限。
- Node 的体积$V_{\text{Node}} \approx V_{\text{runtime}} + V_{\text{modules}}$ 指它需要 Node 运行时加 node_modules,但可以舍弃开发依赖和源代码。
FROM golang:1.21 AS buildWORKDIR /appCOPY go.mod ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o server . FROM scratchCOPY --from=build /app/server /serverENTRYPOINT ["/server"]
FROM python:3.12-slim AS builderWORKDIR /appRUN pip wheel --wheel-dir=/wheels -r requirements.txtCOPY app.py ./ FROM python:3.12-slimWORKDIR /appCOPY --from=builder /app/app.py ./COPY --from=builder /wheels /wheelsRUN pip install --no-index --find-links=/wheels -r requirements.txtCMD ["python", "app.py"]
安全加固实践——不把危险工具留在屋里
装修完工后,如果把电钻、切割机、油漆稀释剂都留在家里,一旦有人破门而入,这些工具就成了顺手可用的凶器。镜像也一样:编译器、包管理器是攻击者最爱的“武器库”。它们平时不说话,但一旦容器被攻破,攻击者就能借助 apt、pip、gcc 下载恶意软件、编译后门。
多阶段构建天然支持“危险工具不进屋”:编译工具只存在于 builder 阶段,最终镜像里只剩运行产物。在此基础上,再叠加几层加固措施——选择尽可能小的基础镜像、用非 root 用户运行、移除包管理器:
攻击面的大小与镜像中无关组件的数量直接相关:
安全是多阶段构建附赠的最大红利之一:不是“顺便瘦身”,而是“刻意断舍离”。把危险工具留在工棚,新家才能住得安心。
- 攻击面$S_{\text{attack}}$ 表示镜像被攻破后可供利用的表面积,单位是“可被利用的组件数量”。
- 组件贡献$C_i$ 是第 $i$ 个组件带来的可被利用的攻击渠道,例如一个包管理器就是一个高危渠道。
- 最小化原则$n \rightarrow \min$ 表示镜像中的组件数越少越好,多阶段构建移除编译器、包管理器,正是从源头减少 $n$ 的值。
FROM golang:1.21 AS builderWORKDIR /appCOPY . .RUN CGO_ENABLED=0 go build -o server . FROM gcr.io/distroless/static-debian12USER nonroot:nonrootCOPY --from=builder /app/server /serverENTRYPOINT ["/server"]
性能调优技巧——装修队并行施工
独自扛着所有工具按顺序施工,和分派水电、木工、油漆三个班组分区并行,速度天差地别。Docker 的 BuildKit 构建引擎(新版 Docker 默认开启)正是这种“并行装修队”——它能识别出无依赖关系的阶段,让它们同时构建。
性能调优不只是“缩小体积”,还包括“缩短时长”。几个实用技巧:
- 开启 BuildKit:
DOCKER_BUILDKIT=1 docker build(新版默认开启),自动跳过最终镜像不需要的阶段。
- 阶段复用:多个服务共用一个基础构建阶段,比如先在
builder里构建公共依赖,再由各子阶段各自 COPY 产物。
- 定向构建调试:
docker build --target=builder只构建某个阶段,避免全量构建浪费时间。
- 体积对比验证:用
docker history <image>检查每层大小,确认是否有意外塞入的巨型层。
并行构建的收益可以用一个简单的模型表达:
施工管理的最高境界,是既快又省。多阶段构建的并行能力配合缓存策略,能让 CI 流水线从“一条长队”变成“几路并进”,为团队省下真金白银。
- 并行总耗时$T_{\text{parallel}}$ 表示多个阶段同时构建时,总耗时只取决于最慢的那个阶段。
- 取最大值$\max\left(T_{s_1}, \ldots, T_{s_n}\right)$ 是各阶段耗时的最大值,对应“水电班组还没干完,油漆组就得等”的场景。
- 与串行对比$\sum_{i=1}^{n} T_{s_i}$ 是各阶段串行耗时之和,两者的差值 $\ll$ 就是并行化省下的时间。
AI 应用场景
AI 推理服务镜像优化
阶段:模型部署 / 服务打包
解决的问题:传统的 AI 推理镜像从 pytorch/pytorch 起步,动辄 3-5GB。这个体积里有训练时才需要的 CUDA 工具链、编译器和各类训练库;而推理阶段只需要 torch runtime + 模型权重。直接分发大镜像,拉取慢、启动慢、安全风险高。
具体案例:某团队将推理 Dockerfile 拆成两段:第一段用 CUDA 构建环境编译自定义算子,第二段用 nvidia/cuda:12.1-runtime 作为底座,只 COPY 编译好的 .so 和模型文件,镜像从 4.2GB 降到 680MB,GPU 节点扩容时间缩短 60%。
MLOps 流水线镜像供应链
阶段:训练任务与数据预处理
解决的问题:训练镜像往往需要频繁迭代,如果每次改一行数据清洗代码就全量重装依赖,代价高昂。多阶段构建的“依赖前置 + 缓存利用”策略,让依赖层缓存永不失效,源码变更只重建最后几层。
具体案例:一个特征工程服务,构建阶段用 python:3.12-slim 安装 pandas、pyarrow 等编译依赖;运行阶段复用相同基础镜像但只 COPY 打包好的 wheel 和脚本,配合 BuildKit 缓存,每次提交的构建时间从 8 分钟压到 50 秒。
模型服务安全合规交付
阶段:模型上线 / 安全审查
解决的问题:金融、医疗机构的容器安全扫描要求镜像中不得出现包管理器或编译器(CVE 高危来源)。审计工具会标记 apt-get、pip、gcc 等潜在风险。
具体案例:某银行 AI 风控系统使用 distroless 基础镜像 + 多阶段构建交付推理服务,Trivy 扫描高危漏洞数为 0,快速通过安全合规评审,省去逐个 CVE 修复的沟通成本。
Python 演示
"""多阶段构建体积与缓存模拟器""" class Layer: """模拟 Docker 镜像中的一个层""" def __init__(self, name, size_mb, runtime=False): self.name = name self.size_mb = size_mb self.runtime = runtime # 是否进入最终运行镜像 BUILDER = [ Layer('基础镜像', 150), Layer('编译工具链', 280), Layer('源码与依赖', 12), Layer('编译产物', 8, runtime=True),] RUNTIME = [ Layer('轻量运行基础', 25, runtime=True), Layer('编译产物', 8, runtime=True),] def single_stage_size(): """单阶段:所有层都会进入最终镜像""" return sum(l.size_mb for l in BUILDER) def multi_stage_size(): """多阶段:只保留运行阶段里标记 runtime 的层""" return sum(l.size_mb for l in RUNTIME if l.runtime) def cache_sim(layers, changed): """模拟层缓存:changed 层出现时,它及之后都要重建""" hit = 0 for name in layers: if name == changed: break hit += 1 return hit, len(layers) - hit def main(): s = single_stage_size() m = multi_stage_size() print('=== 体积对比 ===') print(f'单阶段镜像: {s} MB') print(f'多阶段镜像: {m} MB') print(f'优化率: {(1 - m / s) * 100:.1f}%\n') print('=== 缓存对比(改一行源码) ===') good = ['base', 'install deps', 'copy src', 'build'] bad = ['base', 'copy src', 'install deps', 'build'] hit, rebuilt = cache_sim(good, 'copy src') print(f'依赖前置策略: 命中 {hit} 层, 重建 {rebuilt} 层') hit, rebuilt = cache_sim(bad, 'copy src') print(f'源码前置策略: 命中 {hit} 层, 重建 {rebuilt} 层') if __name__ == '__main__': main()
class Layer:用类封装镜像层,一个层的 name、size_mb、runtime 属性分别对应层名、体积、是否要进入最终镜像。runtime=False默认值表示大多数层(工具链、源码)不该进入最终镜像,只有显式标记的产物才被保留。BUILDER = [...]定义“工棚”阶段的层,包含基础镜像、工具链、源码和产物。RUNTIME = [...]定义“新家”阶段的层,只有轻量运行基础和拷贝进来的产物。multi_stage_size()` 中的 `if l.runtime这是多阶段构建的灵魂:过滤掉所有不需要的层,只统计真正进入最终镜像的体积。cache_sim(good, 'copy src')模拟改一行源码后哪些层命中缓存、哪些层需要重建,展示依赖前置策略的价值。main()依次打印体积对比和缓存对比结果,像装修验收时同时检查“省了多少材料”和“改图纸时返工多少”。
常见误区
误区我只要在 Dockerfile 里多写几个 FROM,镜像体积就能自动变小。FROM 写得越多,镜像越小。
正解多阶段构建只是把“装修”和“入住”隔开的手段,真正让镜像变小的是“最后一个阶段只保留产物并选用小基础镜像”。如果最后一个阶段仍然基于 golang:1.21 或重复安装了一堆编译工具,镜像照样臃肿。FROM 的数量本身不等于优化。
为何常见:教程里往往同时展示“写两个 FROM”和“镜像变小”两个现象,初学者容易把先后关系误当成因果关系。
误区既然 Dockerfile 是“从下往上”构建的,我用了 `FROM golang` 开头,后面就只能继续用 golang 系列镜像,换别的镜像会乱套。
正解多阶段构建的每个 FROM 都是全新的构建环境,彼此之间没有继承关系。你可以第一段用 golang 编译,第二段用 alpine 或 distroless 运行,只要通过 COPY --from 把产物拿过来即可。阶段之间传递的是文件,而不是环境。
为何常见:把多阶段误认为是“一条流水线里叠加指令”,忽略了它其实是“多个独立容器按需搬运文件”。
误区编译器、pip、apt 这些装的依赖放在最终镜像里又不会主动执行,多几百 MB 而已,没什么大不了。
正解这些组件实实在在地增加攻击面:一旦应用被攻破,攻击者可以直接用 apt/pip/gcc 下载和编译恶意程序;安全扫描器也会把包管理器列为高危项。多阶段构建剔除编译工具,既减小体积又提高安全性,是一举两得。
为何常见:本地开发习惯被搬到了生产环境——开发机上装工具是方便,生产镜像里装工具则是隐患。
误区反正都是 COPY 和 RUN,顺序无所谓。把整个项目先拷进去,再安装依赖,每次构建最多重新装一次依赖,能有多大区别?
正解Docker 层缓存的规则是“某层变更则后续全部失效”。把 COPY . . 放前面,任何一行源码改动都会让后面的依赖安装层缓存全部失效,每次都重新下载编译依赖。正确做法是先 COPY 依赖清单、安装依赖,再 COPY 源码,让“常驻工具箱”不因图纸改动而返工。
为何常见:不理解层缓存是按“层顺序”生效的机制,凭直觉认为“先拷贝完整代码再安装”才是完整构建。
误区用了多阶段构建,就代表镜像已经优化到位了,不需要再看基础镜像和包管理。
正解多阶段构建是“手段”而非“终点”。如果最终阶段选用 python:3.12 而不是 python:3.12-slim,或安装依赖时没有清理 pip 缓存、.pyc 文件,体积仍然偏大。多阶段构建后还应检查基础镜像大小、依赖清理情况和层体积分布,才能逼近理论最小体积。
为何常见:把优化“功劳”全部归于某个单一技巧,忽略整体工程的系统性。
面试题
Q1. 多阶段构建中,最终镜像体积主要由哪部分决定?
- A.第一个基础镜像大小
- B.最终阶段保留的层← 正确答案
- C.各阶段体积之和
- D.构建缓存的数量
答案:B。最终镜像只包含最后一个阶段保留的层和通过 COPY --from 引入的产物,前面的阶段都是“工棚”,不影响交付体积。选择 B 是因为它准确指向了多阶段构建的产物链终点;A、C、D 都与最终交付镜像无关。
Q2. 在 Dockerfile 中给构建阶段起名的关键字是什么?
- A.NAME 指令关键字
- B.AS 阶段命名关键字← 正确答案
- C.TAG 镜像标签指令
- D.LABEL 元数据标注
答案:B。FROM xxx AS builder 中的 AS 就是给阶段命名的关键字,之后可以用 COPY --from=builder 来引用。NAME/TAG/LABEL 分别是别的作用域下的关键字,不起阶段命名作用。
Q3. 从上一个阶段复制文件到当前阶段应该使用哪个指令?
- A.ADD --from=builder
- B.COPY --from=builder← 正确答案
- C.RUN --source=builder
- D.MOUNT --stage=builder
答案:B。COPY --from=builder /path /dest 是跨阶段拷贝文件的标准写法。ADD 虽然也能拷贝文件,但 --from 参数是 COPY 指令多阶段构建的标准能力;RUN 和 MOUNT 没有跨阶段拷贝语义。
Q4. 只想单独构建名为 builder 的阶段用于调试,应使用哪个参数?
- A.--target=builder← 正确答案
- B.--stage=builder
- C.--only=builder
- D.--focus=builder
答案:A。docker build --target=builder 可以精准停在 builder 阶段构建完的那一刻,像只验收装修到一半的房子。其余参数均不是 Docker 官方支持的构建定位参数。
Q5. 关于 Docker 层缓存,以下哪项描述正确?
- A.改变一层只会影响该层
- B.改变一层会使后续层缓存失效← 正确答案
- C.镜像层缓存永远不会失效
- D.只有基础镜像才有层缓存
答案:B。Docker 按层顺序构建,某一层内容变化,它之后的所有层无论内容是否变化都会因父层指纹改变而失效。这就像装修时改了一面墙的图纸,后面的工序都得跟着重做。
Q6. 要让 Go 项目获得最小体积的最终镜像,最佳组合是?
- A.golang 基础镜像配合 go run
- B.CGO 关闭加 scratch 镜像← 正确答案
- C.ubuntu 里手动安装 Go
- D.python 运行时打包 Go
答案:B。CGO_ENABLED=0 让 Go 程序静态编译,不依赖动态库,再加上 scratch 空基础镜像,最终二进制就是镜像的全部内容。A、C 会把工具链带进镜像,D 是明显错配。
Q7. 下方 Dockerfile 中,最终镜像包含哪个文件?
- A.只包含编译用的源码
- B.只包含拷贝的 /opt/app← 正确答案
- C.包含 Golang 工具链
- D.包含 Alpine 包管理器
答案:B。最终阶段基于 alpine,只执行了 COPY --from=builder /app /opt/app,因此最终镜像只包含 /opt/app 和 alpine 自身基础文件,Golang 工具链及源码都在 builder 阶段被丢弃。
Q8. 多阶段构建能够带来哪些收益?
- A.减小最终镜像的体积← 正确答案
- B.降低运行时的攻击面← 正确答案
- C.提升构建的缓存利用率← 正确答案
- D.让源码进入最终镜像
答案:ABC。多阶段构建让工具链和源码留在构建阶段,同时缩小体积、降低攻击面、配合依赖前置还能提升缓存命中率。D 恰好与多阶段构建的初衷相反,源码应当尽可能留在构建阶段。
Q9. 以下哪些措施能进一步优化多阶段构建的镜像?
- A.依赖清单先 COPY 再安装← 正确答案
- B.使用 slim 或 distroless← 正确答案
- C.在最终阶段安装编译器
- D.构建后清理包管理器缓存← 正确答案
答案:ABD。依赖前置保持缓存命中;slim/distroless 基础镜像直接减小体积;清理 apt/pip 缓存避免垃圾层。C 在最终阶段安装编译器会显著增大体积并引入安全隐患,是错误做法。
Q10. 关于 BuildKit 并行构建,下列说法正确的有?
- A.阶段间无依赖可并行执行← 正确答案
- B.BuildKit 可跳过无用阶段← 正确答案
- C.所有阶段必定会并行执行
- D.需要较新的 Docker 版本支持← 正确答案
答案:ABD。BuildKit 会在构建图上做依赖分析,没有相互依赖的阶段可以并行跑,也能跳过最终镜像不需要的阶段;但它不是“必定并行”,只有满足依赖条件时才并行。此外 BuildKit 需要新版 Docker 支持,旧版本需手动开启。