多阶段构建:如何生成数百 MB 镜像?

多阶段构建:如何生成数百 MB 镜像?

概述

多阶段构建(Multi-stage Build)是 Docker 17.05 引入的功能,允许在一个 Dockerfile 中使用多个 FROM 语句,每个 FROM 开始一个新的构建阶段。你可以从前一阶段复制文件到后一阶段,而前一阶段的所有中间产物(编译工具、依赖包等)都不会进入最终镜像。

旧时代的痛苦

没有多阶段构建之前

传统做法是使用多个 Dockerfile 或复杂脚本:

# 方案1:一个 Dockerfile,最终镜像巨大
FROM golang:1.21
WORKDIR /build
COPY . .
RUN go build -o myapp .
# 镜像包含 Go 编译器、编译工具链,>1GB
# 方案2:两个 Dockerfile,脚本串联(繁琐)
# Dockerfile.build
FROM golang:1.21 AS builder
WORKDIR /build
COPY . .
RUN go build -o myapp .

# Dockerfile.prod
FROM alpine:3.18
COPY --from=builder /build/myapp /myapp
# 需要外部构建流程来配合
flowchart LR
    subgraph "传统方式(~1.2GB)"
        A1[golang:1.21] --> A2[编译工具+依赖]
        A2 --> A3[二进制作品]
        A3 --> A4["最终镜像\n(包含编译器!)"]
    end

    subgraph "多阶段构建(~15MB)"
        B1["阶段1: golang:1.21"] --> B2[编译]
        B2 --> B3["阶段2: alpine:3.18"]
        B3 --> B4["仅复制二进制\n最终镜像极小"]
    end

多阶段构建实战

Go 应用示例

# === 阶段1:构建 ===
FROM golang:1.21 AS builder
WORKDIR /app

# 利用缓存:先复制依赖文件
COPY go.mod go.sum ./
RUN go mod download

# 构建二进制
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .

# === 阶段2:运行 ===
FROM alpine:3.18
RUN apk --no-cache add ca-certificates tzdata

WORKDIR /app
COPY --from=builder /app/server .

EXPOSE 8080
CMD ["./server"]
docker build -t myapp:latest .
docker images myapp
# REPOSITORY   TAG       IMAGE ID       SIZE
# myapp       latest    abc123def456    15.2MB

Node.js 应用示例

# === 阶段1:安装依赖和构建 ===
FROM node:18-alpine AS builder
WORKDIR /build

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build  # 假设生成 dist/ 目录

# === 阶段2:生产运行 ===
FROM node:18-alpine
WORKDIR /app

# 只复制生产依赖和构建产物
COPY --from=builder /build/node_modules ./node_modules
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/package.json .

EXPOSE 3000
CMD ["node", "dist/server.js"]

Python 应用示例

# === 阶段1:构建依赖 ===
FROM python:3.11-slim AS builder

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    build-essential && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /build
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# === 阶段2:运行 ===
FROM python:3.11-slim

COPY --from=builder /root/.local /root/.local
COPY . /app
WORKDIR /app

# 确保 local/bin 在 PATH 中
ENV PATH=/root/.local/bin:$PATH

CMD ["python", "app.py"]

高级技巧

技巧 1:命名阶段

# 可以给每个阶段命名,通过名字复制
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

FROM eclipse-temurin:17-jre-alpine
COPY --from=builder /build/target/*.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]

技巧 2:多架构编译

FROM --platform=$BUILDPLATFORM golang:1.21 AS builder
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o server .

FROM alpine:3.18
COPY --from=builder /app/server /server
CMD ["/server"]
# 构建多架构镜像
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .

技巧 3:只使用阶段中的部分

FROM ubuntu:22.04 AS base
RUN apt update && apt install -y curl  # 可能很大
RUN echo "shared script" > /script.sh

FROM alpine:3.18
# 只复制脚本文件,跳过 curl
COPY --from=base /script.sh /script.sh
RUN cat /script.sh && rm /script.sh
CMD ["echo", "hello"]

技巧 4:利用构建目标停止

# 如果你只需要编译产物,不需要最终镜像
docker build --target builder -t myapp-builder .
# 构建在 builder 阶段结束后停止

镜像大小对比

flowchart TD
    A[Go Web 应用] --> B[单阶段构建: golang:1.21]
    A --> C[多阶段构建: alpine:3.18]
    A --> D[多阶段构建: scratch]

    B --> B1["~1.2GB"]
    C --> C1["~15MB"]
    D --> D1["~8MB"]

    C1 --> E["减少 98.7%"]
    D1 --> F["减少 99.3%"]

    style B1 fill:#ffcdd2
    style C1 fill:#c8e6c9
    style D1 fill:#c8e6c9
应用类型 单阶段 多阶段(Alpine) 多阶段(Scratch)
Go 二进制 1.2 GB 15 MB 8 MB
Java Spring 700 MB 180 MB ——
Node.js 应用 650 MB 180 MB ——
Python 应用 500 MB 150 MB ——

最佳实践

实践 说明
最小运行镜像 只用 scratchalpinedistroless 作为运行阶段
阶段命名 每个阶段用 AS name 命名,方便复制引用
利用缓存 第一阶段也分层写,先复制依赖后复制源码
不重复安装 工具只在一个阶段安装,不要跨阶段复制
构建时依赖 vs 运行时依赖 编译工具只在构建阶段,运行时镜像只需要运行库
使用 --target 调试 开发时构建到特定阶段,快速调试
注意 COPY --from 路径 路径是相对于前一阶段的工作目录

总结

  • 多阶段构建 = 多个 FROM + COPY --from
  • 第一阶段负责编译、构建、打包
  • 第二阶段(最终阶段)只包含运行所需的最小内容
  • 最终镜像大小可减少 90%-99%
  • 支持构建时跳过不必要的阶段(--target
  • 每个阶段独立缓存,提高构建速度
© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容