Docker 多阶段构建(Multi-stage Build)

Docker 多阶段构建(Multi-stage Build)

核心概念

多阶段构建允许在同一个 Dockerfile 中使用多个 FROM 指令。每个 FROM 开始一个独立的构建阶段,你可以从前面阶段复制文件到后面的阶段——这样能只保留最终需要的文件,大幅缩小镜像体积。

# 第一阶段:构建环境(可能很大)
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp

# 第二阶段:运行环境(非常小)
FROM alpine:latest
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
graph LR
    subgraph 第一阶段: builder
        GOLANG[golang:1.20<br/>800MB]
        BUILD[编译 myapp]
    end

    subgraph 第二阶段: final
        ALPINE[alpine:latest<br/>5MB]
        COPY_BIN[复制编译好的二进制]
    end

    GOLANG --> BUILD -->|COPY --from=builder| COPY_BIN --> FINAL[最终镜像 10MB<br/> 800MB10MB!]

为什么需要多阶段构建?

传统构建的问题

# ❌ 单阶段构建 — 工具链留在镜像中
FROM golang:1.20
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
CMD ["/myapp"]
# 最终镜像
docker images myapp
# myapp  latest   abc123...   800MB  ← 巨大的镜像!
# 其中 790MB 是 Go 编译工具链,运行完全不需要

多阶段构建的解决方案

# ✅ 多阶段构建 — 只复制需要的文件
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp

FROM alpine:latest
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
# 最终镜像
docker images myapp
# myapp  latest   def456...   10MB  ← 小了 99%!

多阶段构建的完整示例

Go 应用

# ====== 阶段1:编译 ======
FROM golang:1.20 AS builder

WORKDIR /src

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

# 复制源码并编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/myapp

# ====== 阶段2:打包 ======
FROM alpine:3.18

# 安装运行时依赖(如果应用需要)
RUN apk add --no-cache ca-certificates tzdata

# 从 builder 阶段复制二进制
COPY --from=builder /app/myapp /myapp

# 设置运行用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

CMD ["/myapp"]

Node.js 应用

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

WORKDIR /build
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

COPY . .
RUN yarn build

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

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

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

Python 应用

# ====== 阶段1:安装依赖 ======
FROM python:3.11-slim AS builder

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/ ./app/

# 确保 PATH 包含 user 安装的包
ENV PATH=/root/.local/bin:$PATH

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

多阶段构建的高级技巧

技巧一:条件构建(多平台)

# 根据不同架构选择不同的基础镜像
FROM --platform=$BUILDPLATFORM golang:1.20 AS builder

ARG TARGETOS
ARG TARGETARCH

WORKDIR /app
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o myapp

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

技巧二:使用多个源

# 从不同阶段获取不同文件
FROM alpine:latest AS base
RUN apk add --no-cache curl

FROM python:3.11-slim AS tools
RUN pip install --user awscli

FROM alpine:latest AS final
# 从 base 复制 curl
COPY --from=base /usr/bin/curl /usr/bin/curl
# 从 tools 复制 awscli
COPY --from=tools /root/.local /root/.local

技巧三:只在某一阶段保留调试工具

# 开发阶段:包含调试工具
FROM node:18-alpine AS development
RUN apk add --no-cache curl vim
WORKDIR /app
COPY . .
CMD ["npm", "run", "dev"]

# 生产阶段:精简
FROM node:18-alpine AS production
COPY --from=development /app/dist ./dist
COPY --from=development /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# 构建不同版本
docker build --target development -t myapp:dev .
docker build --target production -t myapp:prod .

多阶段构建的缓存策略

graph TB
    subgraph 构建流程
        S1[Stage 1: golang:1.20<br/>缓存:  基础镜像]
        S2[Stage 1: COPY go.mod<br/>缓存:  依赖没变]
        S3[Stage 1: RUN go mod download<br/>缓存:  依赖没变]
        S4[Stage 1: COPY . .<br/>缓存:  源码变了]
        S5[Stage 1: RUN go build<br/>缓存:  需要重新编译]

        S1 --> S2 --> S3 --> S4 --> S5

        S6[Stage 2: FROM alpine:latest<br/>缓存:  基础镜像]
        S7[Stage 2: COPY --from=builder<br/>缓存:  二进制变了]

        S5 --> S6 --> S7
    end

镜像大小对比

# 单阶段构建
golang:1.20     -> 800MB
+ 应用二进制       -> 10MB
总计:             810MB

# 多阶段构建
alpine:latest   -> 5MB
+ 应用二进制       -> 10MB
+ ca-certificates -> 3MB
总计:             18MB

# 节省率: 97.8%!

常见错误

错误一:忘记复制必要的运行时文件

FROM golang:1.20 AS builder
COPY . .
RUN go build -o myapp

FROM alpine:latest
# ❌ 没有复制证书和时区
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
# 运行时报错: x509: certificate signed by unknown authority

错误二:在最终阶段中仍然使用了完整的工具链

FROM node:18 AS builder
# ... 构建 ...

FROM node:18
# ❌ 又使用了完整版 node:18(1.1GB)
# 应该用 node:18-alpine (125MB)

FROM node:18-alpine AS final
COPY --from=builder /build/dist ./dist
# ✅ 小很多

错误三:没有利用依赖缓存

# ❌ 每次源码变更都重新下载依赖
FROM golang:1.20 AS builder
COPY . .              # 源码复制在前
RUN go mod download   # 源码变了,缓存失效,重新下载
RUN go build -o myapp

# ✅ 分离依赖和源码
FROM golang:1.20 AS builder
COPY go.mod go.sum ./   # 先复制依赖文件
RUN go mod download      # 缓存最大化
COPY . .
RUN go build -o myapp

面试追问

问:多阶段构建最终的镜像包含了多少个阶段?

答:只包含最后一个阶段以及通过 COPY --from= 从前面阶段复制的文件。前面阶段的基础镜像和层不会存在于最终镜像中。

问:可以在一个 Dockerfile 中用超过两个阶段吗?

答:可以。可以按需使用任意数量的阶段,通常用于:
– 阶段1:编译/构建
– 阶段2:安装测试依赖并运行测试
– 阶段3:生成精简的生产镜像

总结

对比项 单阶段构建 多阶段构建
最终镜像大小 大(包含工具链) 小(只包含所需)
安全性 低(包含多余工具) 高(减少攻击面)
构建复杂度 中(需要设计阶段分离)
构建速度 好(缓存利用率高)
推荐 ✅ 必须使用

一句话总结:多阶段构建是「把大象装冰箱」——在第一个冰箱里装进大象,然后只把需要的肉拿出来放进第二个小冰箱里,大冰箱就可以扔了。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容