Skip to content

多阶段构建(Multi-stage Build)

是什么

Dockerfile 17.05+ 引入的特性。一个 Dockerfile 里可以写多个 FROM 指令,每个 FROM 开启一个新的构建阶段,前一阶段的产物可以被后续阶段选择性 COPY --from 进来,最终只输出最后一个阶段的镜像。

传统构建:                                    多阶段构建:
┌──────────────────────┐                    ┌──────────────────────┐
│ FROM golang:1.22      │                    │ FROM golang:1.22 AS build│
│ RUN go build          │                    │ RUN go build          │
│ # 编译工具全留在镜像里 │                    │ COPY --from=build ...  │
│ # 镜像 800MB+         │                    │ FROM alpine           │
└──────────────────────┘                    │ # 镜像 20MB           │
                                            └──────────────────────┘

本质目的:让编译环境和运行环境解耦,扔掉所有构建期依赖。

三个核心机制

机制 作用 关键语法
AS <名字> 给当前阶段起名,后续 --from 引用 FROM golang:1.22 AS build
COPY --from=<阶段> 从指定阶段拷贝文件到当前阶段 COPY --from=build /app/myapp .
隐式阶段编号 不命名时按 0, 1, 2... 自动编号 COPY --from=0 ...
FROM golang:1.22 AS build      # 显式命名 build
FROM node:18  AS frontend      # 显式命名 frontend
FROM alpine:3.19               # 最后一个 FROM 不用命名,是最终镜像

COPY --from=build    /app/server  /bin/
COPY --from=frontend /app/dist    /www/

PS:

  • AS 名字必须小写,不能大写(Docker 规范)
  • 只有最后一个阶段会被作为最终镜像输出,前面的全部丢弃
  • 中间阶段仍会作为镜像缓存层保存在本地(用 docker images 能看到 <none> 标签的中间镜像),可以用 docker build --target <阶段名> 单独输出某一阶段

实战模板

Go 二进制

# 阶段一:编译
FROM golang:1.22 AS build
WORKDIR /src
# 先 copy go.mod 单独一层,依赖没变就不重新 go mod download
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app .

# 阶段二:scratch 极简运行时
FROM scratch
COPY --from=build /out/app /app
# scratch 没有 ca-certificates,访问 https 要复制
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/app"]
# 最终镜像通常 < 20MB
docker build -t myapp:v1 .
docker images myapp

Node 前端 + Go 后端 同构建

# 阶段一:编译前端
FROM node:18-alpine AS frontend
WORKDIR /web
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build         # 输出到 /web/dist

# 阶段二:编译后端
FROM golang:1.22 AS backend
WORKDIR /go/src/app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server .

# 阶段三:最终运行时(前端产物嵌进后端镜像)
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
COPY --from=frontend /web/dist   /www/dist
COPY --from=backend  /out/server /bin/server
EXPOSE 8080
ENTRYPOINT ["/bin/server"]

阶段作为测试环境

FROM golang:1.22 AS test
WORKDIR /src
COPY . .
RUN go test ./...            # 这一阶段失败 = 整个 build 失败

FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN go build -o /out/app .

FROM alpine:3.19
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
# 想跑测试不构建最终镜像
docker build --target test -t myapp:test .
docker run --rm myapp:test

进阶技巧

1. 复用 ARG 跨阶段

ARG VERSION=1.0.0
ARG GO_VERSION=1.22

FROM golang:${GO_VERSION} AS build
ARG VERSION
RUN go build -ldflags="-X main.version=${VERSION}" -o /out/app .

FROM alpine:3.19
ARG VERSION
LABEL version=${VERSION}
COPY --from=build /out/app /app

2. 外部镜像作为阶段源

--from 不只能引用当前 Dockerfile 的阶段,还能直接用任意已存在的镜像

FROM alpine:3.19
# 直接从官方 nginx 镜像里挖配置文件
COPY --from=nginx:1.25 /etc/nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./dist /usr/share/nginx/html

用途:从官方镜像里挑工具用,避免自己装。

3. 利用构建缓存

构建缓存按指令顺序文件内容 hash 命中。多阶段构建的缓存策略:

FROM golang:1.22 AS build
WORKDIR /src

# 先 copy 依赖文件 → 这一层独立缓存
COPY go.mod go.sum ./
RUN go mod download

# 再 copy 源码 → 源码改了不影响 go mod download 那层
COPY . .
RUN go build -o /out/app .
源码改动      go mod download 那层     go build 那层
              ↓ 已缓存,跳过             ↓ 重新执行
时间: 30s →  1s + 5s = 6s              (而不是 30s+)
# 跨机器复用缓存
docker buildx build \
  --cache-to type=registry,ref=ghcr.io/me/myapp:cache,mode=max \
  --cache-from type=registry,ref=ghcr.io/me/myapp:cache \
  -t myapp:v1 .

4. BuildKit 内联构建(更现代的写法)

Docker 23+ 配合 BuildKit,支持单 RUN 内多阶段、挂载缓存等:

# syntax=docker/dockerfile:1.7
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=bind,source=go.sum,target=go.sum \
    CGO_ENABLED=0 go build -o /out/app .
挂载类型 作用
type=cache 编译缓存持久化到独立卷,构建间复用
type=bind 精确控制哪些文件变更触发重建
type=tmpfs 临时内存盘(如挂 /tmp
type=secret 注入密钥,不入镜像层(如 npm token)

最佳实践

实践 为什么
最终阶段用 alpinescratch 基础镜像小,CVE 少
CGO_ENABLED=0 编译 Go 产出静态二进制,可直接跑在 scratch
-ldflags="-s -w" 去掉符号表和 DWARF,体积再缩 ~25%
阶段名字用动词或角色 build / frontend / test,比 stage1 清晰
每个 FROM精确 tag 别用 latest,构建不可复现
依赖文件先 COPY 单独一层 利用缓存,源码改动不重装依赖
COPY 而非 ADD ADD 会自动解压还支持 URL,副作用多
多阶段别超过 3-4 段 阶段太多 Dockerfile 难读,缓存粒度也细
最终镜像不装 curl/wget 调试可以临时 --target build 进去看

常见坑

scratch 镜像跑不起来

scratch 是空镜像,没有 /etc/passwd/etc/ssl/certs/lib/ld-linux.so 等:

症状:
exec /app: no such file or directory      # 动态链接库缺失
x509: certificate signed by unknown authority  # ca 证书缺失

解决:
1. 用 CGO_ENABLED=0 编译,产出纯静态二进制
2. 复制 ca-certificates
3. 非必要别用 scratch,用 alpine 更省心

中间阶段镜像堆积

每次构建都生成一堆 <none> 标签的中间镜像:

docker images | grep none
docker image prune -f    # 清掉 dangling 镜像
# 想全清(包括 build cache)
docker builder prune -af

用了 BuildKit 特性但 engine 不支持

# 现象:--mount=type=cache 报 unknown flag
# 解决:
DOCKER_BUILDKIT=1 docker build .            # 旧 docker
# 或直接装 docker 23+,默认开 BuildKit

参考