为什么多阶段构建

多阶段构建将“编译阶段”和“运行阶段”拆分,最终镜像仅包含运行所需的产物,大幅降低体积与攻击面,同时配合缓存策略缩短构建时间、提升 CI 稳定性。

基础示例(Go)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 构建阶段
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o app ./cmd/app

# 运行阶段(精简镜像)
FROM gcr.io/distroless/base-debian12
COPY --from=build /src/app /app
USER 65532:65532
ENTRYPOINT ["/app"]

要点:

  • 先复制 go.mod/go.sum 再 download,最大化利用缓存
  • 使用 -ldflags="-s -w" 去除符号表缩小体积
  • Distroless 无 shell,安全面更小

Node/前端镜像示例

1
2
3
4
5
6
7
8
9
10
11
# 构建阶段
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build

# 运行阶段(仅复制静态产物)
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

Java 示例(分层与JRE)

1
2
3
4
5
6
7
8
9
10
11
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
COPY pom.xml .
RUN mvn -B -q -e -DskipTests=true dependency:go-offline
COPY . .
RUN mvn -B -q -DskipTests=true package

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /src/target/app.jar app.jar
ENTRYPOINT ["java","-XX:+UseContainerSupport","-jar","/app/app.jar"]

BuildKit/缓存/多架构

启用 BuildKit 与缓存挂载:

1
DOCKER_BUILDKIT=1 docker build --progress=plain .

在 Dockerfile 中使用缓存(Go 模块):

1
2
3
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o app ./cmd/app

构建多架构镜像:

1
2
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t repo/app:1.0 --push .

.dockerignore 与层优化

  • 忽略不必要的文件(.git、node_modules、测试数据、临时产物)
  • 将变化频率高的文件靠后 COPY,减少层失效
  • 合并 RUN 层,清理包管理器缓存
1
2
RUN apk add --no-cache curl && \
rm -rf /var/cache/apk/*

安全与合规

  • 非 root 用户运行(USER
  • 只读根文件系统与最小权限
  • 使用 Trivy/Grype 做漏洞扫描
1
trivy image repo/app:1.0
  • SBOM 产出与签名
1
2
syft repo/app:1.0 > sbom.json
cosign sign --key cosign.key repo/app:1.0

运行与可观测性

  • 正确处理信号:ENTRYPOINT 使用可转发信号的 init(如 distroless 默认支持,或 tini
  • 暴露健康检查
1
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/health || exit 1

常见踩坑

  • 将 package.lock/yarn.lock 忽略导致缓存失效严重
  • 在运行镜像内执行 shell 指令失败(distroless 无 sh),调试需在构建镜像或临时替换为 alpine
  • 时区/证书缺失:按需安装 tzdata 或 CA 证书
  • 层顺序差导致频繁全量重建

FAQ

  • Alpine 与 Distroless 选哪个?Alpine 可调试;Distroless 更安全更小。开发阶段 Alpine,生产可切 Distroless
  • 如何加速构建?并行构建、缓存挂载、镜像代理(registry mirror)
  • 镜像太大?排查大文件(dive 工具)、压缩产物、剔除无关二进制与文档

总结

多阶段构建是现代容器化的“标配”。结合 BuildKit 缓存、多架构构建、非 root 安全策略与 SBOM/签名流程,既能获得小体积高安全的镜像,又能保障交付效率与可追溯性。