Docker 多阶段构建

一个镜像若同时装构建工具和运行依赖,体积会膨胀到几百 MB。多阶段构建用多个 FROM 分阶段组装,最终只保留运行所需的产物,是镜像瘦身最有效的手段。

为什么要多阶段

多阶段构建示意 编译型应用(Go/Java/C++)需要完整的工具链才能构建,但运行时只需要产物与少量依赖。单阶段方案要么把 gcc、JDK 等一起塞进最终镜像,要么靠手工删文件——多阶段构建用一条 Dockerfile 优雅解决:

方案最终镜像内容体积
单阶段(一个 FROM)工具链 + 源码 + 产物数百 MB
多阶段(多个 FROM)仅产物与运行依赖数十 MB 甚至更小

基本语法

每个 FROM 开启一个阶段,可用 AS 起名,后一阶段用 COPY --from=阶段名 只拷贝指定文件:

FROM golang:1.22-alpine AS builder   # 第一阶段:命名 builder
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/hello .

FROM alpine:3.20                     # 第二阶段:全新精简基础
WORKDIR /app
COPY --from=builder /app/hello .     # 只从 builder 拷编译产物
EXPOSE 8080
CMD ["./hello"]
docker build -t hello-multi .
docker run --rm hello-multi
# 输出:Hello, Docker!(Go 程序运行结果)

大小对比

用 docker images 看结果:golang:1.22-alpine 基础镜像就占约 250 MB,而上面的最终镜像只有几 MB 到十几 MB:

docker images | grep hello-multi
# 输出:hello-multi   latest   ...   12.4MB
docker history hello-multi --no-trunc | head   # 可见历史里只有运行阶段

CGO_ENABLED=0 编译出的纯静态二进制,甚至可以直接放进 scratch 空镜像,做到最小。

构建期缓存与依赖分离

多阶段里同样利用层缓存:把 go.mod 单独 COPY 并先 go mod download,源码不变时依赖层直接命中缓存,改造后重新构建通常只要几秒:

FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download          # 依赖只下载一次,后续走缓存
COPY . .
RUN CGO_ENABLED=0 go build -o /app/hello .

只构建到某一阶段

调试时可用 --target 停在指定阶段,例如只想看构建产物、不跑运行阶段:

docker build --target builder -t hello-builder .

小结

多阶段构建 = 多个 FROM + AS 命名 + COPY --from 取产物:构建阶段随便装工具,运行阶段用精简基础镜像只拷产物,最终镜像小、层少、攻击面也小,生产 Dockerfile 应默认采用。

笔记加载中…