多阶段构建通过将构建过程拆为多个阶段,每个阶段基于不同基础镜像,最终只把必要产物复制到最终镜像,因此能大幅缩小容器镜像体积。这种做法的核心在于把编译环境和运行环境彻底分离,避免把构建工具链、临时文件等冗余内容打包进最终镜像,与传统单阶段构建相比,镜像体积常能缩减 60% 以上,甚至更多,下面从机制、对比、实战场景和效率等角度拆解,看看这个技术为什么能成为镜像瘦身的利器。
多阶段构建为什么能显著减小容器镜像的体积
传统构建方式的痛点
在引入多阶段构建之前,常见的做法是在一个 Dockerfile 里用同一个基础镜像完成代码编译、依赖安装、应用打包等所有步骤,这种做法导致最终镜像包含大量只在编译时需要的工具,gcc、make、node_modules 中的开发依赖、临时下载的源码包等,这些内容在运行时完全用不到,却白白占用空间。
- 基础镜像通常很大,ubuntu 带编译工具链可能超过 1GB。
- 依赖安装过程会生成缓存和临时文件,清理不彻底。
- 最终镜像里既有编译工具又有运行环境,层次混乱,难以复用。
行业共识认为,传统单阶段构建的镜像中,有相当一部分内容是冗余的,真正运行所需的文件占比往往不到 10%,这直接导致镜像拉取和部署变慢,存储成本上升。
多阶段构建的核心机制
多阶段构建允许在同一个 Dockerfile 中定义多个 `FROM` 指令,每个 `FROM` 代表一个独立的构建阶段,每个阶段可以基于不同的基础镜像,执行不同的操作,最终镜像只保留最后一个阶段的内容,前面阶段产生的文件可以通过 `COPY --from=<阶段名>` 选择性复制到最终阶段。
- 构建阶段:使用完整的开发镜像,安装编译工具、下载依赖、执行编译。
- 最终阶段:使用极简的运行镜像,如 alpine、distroless,只复制编译好的二进制文件或打包产物。
- 所有中间产物、临时文件、构建工具都不会进入最终镜像。

这种机制从根源上切断了冗余内容的进入路径,让镜像体积实现质的飞跃,一个 Go 应用如果用 alpine 编译,最终镜像可以控制在 10MB 以内,而单阶段构建可能超过 300MB。
多阶段构建与单阶段构建镜像大小对比
构建过程差异
单阶段构建是线性的,所有指令在一个上下文中执行,难以分离构建和运行依赖,多阶段构建则是分段的,每个阶段独立,互不干扰。
| 对比项 | 单阶段构建 | 多阶段构建 |
|---|---|---|
| 基础镜像选择 | 只能用一个基础镜像,通常为开发镜像 | 不同阶段可自由选择不同镜像 |
| 构建工具 | 必须保留在最终镜像中,或手动清理 | 构建阶段持有,最终镜像不包含 |
| 临时文件 | 难以彻底清除,常残留 | 完全不进入最终镜像 |
| 缓存利用 | 依赖同一镜像层,缓存粒度粗 | 每个阶段独立缓存,复用性好 |
多阶段构建在构建过程中天然隔离了不同角色,让最终镜像只携带运行时所需的最小文件集。一个 Java 应用在构建阶段使用 maven:3.8-jdk-11,最终阶段使用 openjdk:11-jre-slim,通过 COPY 复制 war 包,避免了 jdk 和 maven 依赖的冗余。
最终镜像大小差异
以一个典型的 Node.js 应用为例,单阶段构建:
- 使用 node:18 基础镜像,约 300MB。
- 安装 npm 依赖,包括开发依赖,约 150MB。
- 构建产物,约 10MB。
- 最终镜像大小约 460MB。
多阶段构建:
- 构建阶段:使用 node:18,安装依赖并构建,产生 460MB 的中间层,但不会保留。
- 最终阶段:使用 node:18-alpine,约 50MB。
- 仅复制构建产物和生产依赖,最终镜像大小约 80MB。
体积差异高达 5 倍以上。对于大型项目,这种优势更加明显,据统计,采用多阶段构建后,镜像体积一般能减少 70% 以上,推送和拉取时间也相应缩短。

多阶段构建在微服务场景下的优化实践
分离构建环境与运行环境
微服务架构中,每个服务独立部署,镜像体积直接影响部署速度和资源利用率,多阶段构建非常适合分离构建与运行环境。
- 构建阶段:使用包含完整 SDK 的镜像,确保编译和测试顺利。
- 运行阶段:使用精简镜像,如 alpine、debian-slim,甚至 distroless。
- 通过 COPY --from 复制编译产物,确保运行环境干净。
一个 Spring Boot 微服务,构建阶段使用 maven:3.8-eclipse-temurin-17,运行阶段使用 eclipse-temurin:17-jre-alpine,最终镜像只包含 JRE 和 fat jar,体积从 400MB 减到 100MB 左右。
减少依赖冗余
微服务常常共享基础库,但不同服务可能依赖不同版本,多阶段构建可以在每个服务的构建阶段灵活处理依赖,最终镜像只包含运行时必需的库。
- 对于 Python 应用,构建阶段安装依赖并生成 requirements.txt 的精确版本,最终阶段通过 pip install --no-cache-dir 仅安装生产依赖。
- 对于 C++ 应用,构建阶段编译静态库,最终阶段直接复制二进制文件,避免动态链接库的额外依赖。
这种按需打包的方式,让每个微服务镜像都保持最小化,整体存储和传输成本显著降低。国内使用多阶段构建时,还可以结合简米云镜像加速器或酷番云容器服务,进一步优化拉取速度。
多阶段构建是否影响构建时间
构建缓存利用
多阶段构建的每个阶段都有独立的缓存层,当 Dockerfile 中的指令没有变化时,对应阶段可以直接复用缓存,不会重复执行。
- 构建阶段的依赖安装层一旦缓存,即使后续代码修改,只要依赖文件不变,该层就不会重跑。
- 最终阶段的 COPY 指令只复制新产物,层数少,缓存命中率高。
多阶段构建的构建时间通常不会增加,反而因为缓存复用而比单阶段更高效。

因为单阶段构建一旦修改了依赖相关层,后续所有层都可能失效,而多阶段构建把依赖和编译分离,减少了缓存失效范围。
并行构建策略
多阶段构建理论上支持在 CI/CD 流水线中并行执行某些阶段,但 Docker 默认是顺序执行的,通过合理设计阶段顺序,可以让耗时长的编译步骤尽早开始。
- 把依赖安装和代码编译放在不同阶段,利用 Docker 的构建缓存机制,让不变的部分快速跳过。
- 对于大型项目,可以拆分多个构建阶段,分别编译不同模块,最后合并产物。
行业共识认为,多阶段构建在大多数场景下不会显著增加构建时间,甚至可以通过优化缓存和并行策略缩短总时间,如果遇到构建时间延长,通常是因为基础镜像拉取较慢,可以考虑使用国内镜像源或提前拉取基础镜像。
多阶段构建减小镜像体积常见问题
多阶段构建能否用于所有编程语言?
可以,但具体实现方式因语言而异,对于编译型语言(Go、Rust、C++),构建阶段编译出二进制文件,最终阶段使用 scratch 或 alpine 即可,对于解释型语言(Python、Node.js),需要复制生产依赖和代码,但也可以通过工具压缩依赖,对于 Java,需要区分 JDK 和 JRE,构建阶段用 JDK,最终阶段用 JRE。
如何选择基础镜像来平衡体积与安全性?
优先选择官方镜像或 distroless 镜像,它们体积小且经过安全审计,alpine 镜像体积小但使用 musl libc,与 glibc 不完全兼容,测试要充分,distroless 镜像只包含应用运行时所需的最小系统组件,不含 shell 和包管理器,进一步缩小体积并降低攻击面。
多阶段构建是否增加了 Dockerfile 的维护难度?
初期需要额外规划阶段划分,但一旦结构固定,维护成本很低,常用做法是把构建阶段写成通用模板,不同项目只需调整基础镜像版本和复制路径,相比体积优化带来的收益,这点维护成本完全可以接受。