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 应默认采用。