多阶段构建能显著减小容器镜像体积,核心答案在于它允许你在最终镜像中只保留运行时所需的产物,而彻底丢弃构建过程中产生的一切中间文件和工具链。
以往构建镜像,我们习惯用一个大而全的基础镜像,装编译器、拉源码、编依赖,最后把这些「作案工具」连同产物一起打包进镜像,这就像做完饭不刷锅,把菜板、菜刀、甚至垃圾桶一起端上桌,多阶段构建的逻辑很简单:想清楚你真正需要什么,其余全部扔掉。
多阶段构建和普通构建有什么区别
普通构建方式,业内称之为「单阶段构建」,它的流程是:选择一个基础镜像 → 安装依赖 → 拷贝代码 → 执行构建 → 生成镜像,整个过程在一个临时容器里完成,所有层都被永久写入最终镜像。
多阶段构建则完全不同,它在同一个 Dockerfile 里使用多个 FROM 指令,每一段都是一个独立的构建环境,关键点在于:后面的阶段可以引用前面阶段的文件,但不会继承前面阶段的环境,换句话说,你可以在第一个阶段里装一个 5GB 的 Golang 编译器,在第二个阶段只需要用一条 COPY --from=0 把编译好的二进制文件拷过来就行。
下表可以直观看出两者的差异:
| 对比维度 | 普通单阶段构建 | 多阶段构建 |
|---|---|---|
| 构建环境 | 与最终运行环境混用 | 构建环境与运行环境完全隔离 |
| 镜像体积 | 往往在数百MB到数GB | 可缩小至数十MB甚至几MB |
| 安全风险 | 暴露大量无用软件包和漏洞面 | 攻击面大幅收窄 |
| 构建时间 | 每次改动依赖需全量重来 | 各阶段有独立缓存,可精准复用 |
单阶段构建的痛点不仅在于体积大,如果你的应用需要 CGO(C语言动态链接库),你还得保证运行镜像里有对应的 .so 文件,普通构建把所有东西装在一起,很容易出现版本冲突,多阶段构建通过隔离环境,让每个阶段各司其职,构建阶段负责编译,运行阶段只负责执行,问题自然消解。
以一条典型的 Go 服务为例,普通构建可能是这样的:
FROM golang:1.21 WORKDIR /app COPY . . RUN go build -o myapp . CMD ["./myapp"]
这样构建出来的镜像体积通常在 800MB 到 1GB 之间,因为 golang:1.21 基础镜像本身就超过 800MB,加上编译缓存和源码,体积相当可观。
换成多阶段构建:
# 阶段一:专门用于编译 FROM golang:1.21 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o myapp . # 阶段二:仅用于运行 FROM alpine:3.19 WORKDIR /root/ COPY --from=builder /app/myapp . CMD ["./myapp"]
最终镜像体积能压到
20MB 左右,缩小了整整一个量级,这个压倒性的差异就是多阶段构建的价值体现。
多阶段构建dockerfile怎么写
动手写一个多阶段构建的 Dockerfile,思路比命令本身更重要,你需要先问自己两个问题:构建这个程序需要哪些工具?运行这个程序又需要哪些东西? 答案之间的差值,就是你省掉的空间。
我们分场景看两个具体的写法。
编译型语言应用(以 Java 为例)
Java 应用的构建过程涉及 Maven 或 Gradle 下载大量依赖,这些依赖缓存动辄几百兆,但运行时根本用不到,多阶段构建可以这么整理:
# 第一阶段:用 Maven 镜像做编译 FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 第二阶段:用 JRE 镜像做运行 FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=build /build/target/myapp.jar . EXPOSE 8080 ENTRYPOINT ["java", "-jar", "myapp.jar"]
- 第一阶段使用完整的 Maven 镜像,它有编译器和全部依赖管理工具。
- 第二阶段换用精简的 JRE 运行镜像,只有 Java 运行时环境。
COPY --from=build直接将编译好的myapp.jar拿过来,构建阶段的其他文件一概不带入。
解释型语言应用(以 Python 为例)
Python 应用虽然不用编译成二进制,但依赖安装过程会在镜像里留下 pip 缓存和中间文件,多阶段构建照样适用:
# 第一阶段:安装 Python 依赖 FROM python:3.11-slim AS dependencies WORKDIR /app COPY requirements.txt . RUN pip install --prefix=/install -r requirements.txt # 第二阶段:精简运行环境 FROM python:3.11-slim WORKDIR /app COPY --from=dependencies /install /usr/local COPY . . CMD ["python", "app.py"]
这里有个关键操作:pip install --prefix=/install 把所有依赖安装到指定目录,之后只复制这个目录,连 pip 本身和它产生的缓存都不带进最终镜像。
前端项目镜像优化
前端项目的构建更是完美匹配多阶段构建的场景,Node 环境动辄几百兆,但最终产物只是一堆静态文件。
# 阶段一:安装依赖并构建 FROM node:20-alpine AS builder WORKDIR /app COPY package.json ./ RUN npm ci COPY . . RUN npm run build # 阶段二:用 Nginx 托管静态资源 FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80
这个做法在业内有相当一部分团队在用,前端构建的 node_modules 通常超过 500MB,但最终发布到生产环境的只是 dist 目录里几十兆的静态资源,通过多阶段构建,中间那 500MB 的依赖目录直接人间蒸发。
多阶段构建能减小镜像体积的核心机制
有些人以为多阶段构建只是「换了个更小的基础镜像」,这误会可大了,基础镜像的优化固然是其中一环,但更深层的原因在于
分层机制的巧妙利用。
Docker 镜像由多个只读层组成,每一层都是在上层基础上增加的文件变更,单阶段构建时,每一行指令都会生成一个新的镜像层,这些层即便之后被删除或覆盖,依然会留存在镜像历史中,举个例子:
RUN wget http://example.com/big-tool.tar.gz &&
tar xzf big-tool.tar.gz &&
rm big-tool.tar.gz
哪怕你删掉了那个压缩包,这一层的所有数据依然完整保存在镜像中,下一层只是在它上面打了一个「删除」标记,镜像体积不会减少,反而会因为多了一层的元数据而变大,这是单阶段构建体积失控的根本原因,也是多阶段构建最大的优势所在:构建过程中的临时文件物理上与最终镜像无关。
多阶段构建还天然解决了另一个问题:构建上下文隔离,第一阶段可以包含海量的源码测试文件、开发工具链、调试符号,这些在第二阶段统统不会出现,最终镜像从诞生的那一刻起就长成瘦身后的样子,不存在「删除」过程,所以也没有历史包袱。
行业共识认为,多阶段构建是最有效、最易执行、成本最低的镜像优化手段,对比其他优化方法docker-slim 自动瘦身工具、手工清理 apt 缓存、合并 RUN 指令多阶段构建更可控、更可维护,而且不需要任何外部工具依赖。
多阶段构建镜像优化后还需注意什么
体积瘦下来只是第一步,镜像体积优化到极致之后,你还需要在意构建效率、缓存策略乃至安全扫描的实际效果。
构建缓存的有效利用
多阶段构建并不是写得越复杂越好,如果每个阶段都从零开始,构建时间可能反而变长。合理排序指令是让缓存生效的关键。
以 Node 项目为例,把 COPY package.json ./ 放在 COPY . . 之前,这样只有当依赖清单文件变化时才会重新执行 npm install,代码文件频繁变化,但 package.json 一般不会经常变,大部分情况下可以命中缓存层,大幅缩短构建时间。
多平台的体积差异
在不同 CPU 架构上构建镜像,体积表现也有差异。arm64 架构的某些基础镜像比 amd64 的略小,多阶段构建时可以根据目标运行平台选择不同的基础镜像,如果你想同时支持多平台,Docker 的 buildx 插件配合多阶段构建可以一次产出多个架构的镜像,但需要注意:
- 编译型语言需要交叉编译支持,Go 的
GOOS和GOARCH环境变量。 - 部分基础镜像不提供特定架构的变体,提前确认避免构建失败。
安全扫描
镜像体积缩小意味着攻击面缩小,但最终镜像里的应用二进制和运行时依赖,仍需定期扫描漏洞,借助 Docker Scout 或 Trivy 这类工具,可以持续监控镜像安全性。体积小不等于绝对安全,但更大的镜像通常包含更多未知组件,对应更大的风险暴露面。
多阶段构建和单阶段构建的区别能带来多大性能提升
从实际部署效果来看,镜像体积减少意味着分发速度提升、启动时间缩短、存储成本下降,一个 1GB 的镜像和一个 20MB 的镜像,在跨地域拉取时的体验差异是巨大的。
带宽有限的场景下,比如边缘计算节点或内网环境,这种差距尤其明显,想象一下:你在简米云华北区构建了一个镜像,需要在华南区的服务器上部署,1GB 的镜像可能耗时分钟级,而 20MB 的镜像秒级即可完成,在 Kubernetes 集群中水平扩容时,如果节点需要拉取镜像,小体积镜像能在几秒内完成调度,反之则可能拉长到几分钟。
业内专家指出,多数生产环境故障发生在镜像更新或扩容期间,拉取时间长会显著延长服务不可用的窗口期,多阶段构建虽然不能根治所有运维问题,但它是成本最低、见效最快的优化手段。
多阶段构建能显著减小容器镜像体积吗
能,而且通常能减小一个量级以上。 这不是理论推演,而是所有主流容器实践都验证过的结论,无论你用的是 Docker、Podman 还是 Kubernetes 本地的 containerd,多阶段构建的语法和原理都完全一致。
在实际使用中,有几个常见的坑值得一提:
- 不要在一个阶段里同时安装编译器和运行时依赖,否则又回到了单阶段的老路。
- 尽量把
COPY --from拷贝的内容控制在最小范围,比如单独复制可执行文件,而不是整目录复制。 - 构建阶段的基础镜像选择也需谨慎,虽然它不进入最终镜像,但会影响构建速度和缓存命中率。
多阶段构建的核心哲学就是「隔离构建环境,只交付运行成果」,正如我们把做饭和吃饭分开,厨房再乱,端上桌的菜是精致干净的就对了,你的镜像体积问题,本质上是没有把这两个环节拆开。
常见问题解答
多阶段构建会影响本地开发调试吗
不会,多阶段构建只影响 docker build 的过程,对本地源代码、开发环境、测试流程没有任何侵入性,你依然可以在本机直接运行 npm run dev 或 go run main.go,多阶段构建只是定义了一条构建路径,不是一种运行模式。
多阶段构建中能不能通过变量控制构建阶段
可以,Dockerfile 支持使用 ARG 配合 AS 命名阶段,并在 --target 参数中指定仅构建某个阶段,在调试时你可以只构建 builder 阶段,查看中间产物;正式发布时再构建完整链路,灵活性较高。
多阶段构建是使用 alpine 镜像的替代方案吗
两者不是同一层次的概念,多阶段构建解决的构建过程与运行环境隔离的问题,alpine 解决的是基础镜像大小的问题,两者可以叠加使用,最佳实践往往是在构建阶段用完整功能的基础镜像以保证兼容性,在运行阶段仅用精简的 alpine 镜像来托管产物。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/641072.html





