「每日代码」Docker 多阶段构建实战

实战案例 猫同学 20 5 月, 2026 📖 7 分钟 👁 13

简介

Docker 多阶段构建(Multi-stage Builds)是 Docker 17.05 引入的一项强大特性。它允许你在一个 Dockerfile 中使用多个 FROM 指令,每个指令开启一个新的构建阶段,你可以选择性地将前一阶段的产物复制到下一阶段,最终镜像只保留运行时必需的组件。

这样做的好处非常明显:镜像体积大幅缩减。编译工具链、中间产物、源码文件统统被丢弃,最终镜像只包含编译好的二进制文件或静态资源,体积通常可以减少 70% 到 90%。

核心概念

传统构建的痛点

在没有多阶段构建之前,开发者通常用两种方式处理:

  1. 单 Dockerfile + 清理脚本:在同一个 Dockerfile 里安装编译工具 → 编译 → 删除工具和源码。这样即使删除了,历史层仍然存在,镜像体积下不来。
  2. 构建器模式:用一个 Dockerfile 编译,再用另一个 Dockerfile 打包产物。虽然解决了体积问题,但需要维护两个 Dockerfile 和一个编排脚本,流程割裂。

多阶段构建优雅地融合了这两种方式的优点:一个 Dockerfile,多个 FROM,按需复制

核心语法

FROM image AS stage-name     # 命名构建阶段
COPY --from=stage-name ...   # 从其他阶段复制文件

每个 FROM 开启一个新的阶段,之前的层历史被完全丢弃。COPY --from 可以引用之前的任何阶段(通过索引 0, 1, 2… 或阶段名)。

代码示例

示例 1:Go 应用(体积减少 95%)

Go 是静态编译语言,编译后只有一个二进制文件,是多阶段构建的完美案例。

# 阶段 1:编译
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .

# 阶段 2:运行
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /app/server /usr/bin/server
EXPOSE 8080
CMD ["server"]

最终镜像只有约 12MB,而 golang:1.22-alpine 基础镜像就有 300MB+。关键在于 -ldflags="-s -w" 去掉调试信息进一步缩小二进制体积。

示例 2:Node.js 前端(只保留静态文件)

前端项目构建后只需要 HTML、CSS、JS 静态文件,用 Nginx 做 Web 服务器是标准方案。

# 阶段 1:npm build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build

# 阶段 2:Nginx 托管
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

最终镜像不到 20MB,且不含任何 Node.js 运行时或 node_modules。

示例 3:Python 应用(分离依赖安装)

Python 的 pip 包可能包含编译步骤(如 C 扩展),把依赖安装和运行分开能显著减重。

# 阶段 1:安装依赖
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 阶段 2:运行
FROM python:3.12-slim
COPY --from=builder /root/.local /root/.local
COPY app/ /app/
ENV PATH=/root/.local/bin:$PATH
WORKDIR /app
CMD ["python", "main.py"]

这里用 pip install --user 将依赖装在 /root/.local,然后直接拷贝到运行阶段,避免了在运行镜像中再安装 pip 包。

最佳实践

  1. 给每个阶段命名:用 AS name 比用索引号可读性更好,尤其是在有 3 个以上阶段时。
  2. 基础镜像选最小版本:优先使用 alpineslim 标签作为最终运行阶段。
  3. 利用构建缓存:先 COPY 依赖文件(package.json、go.mod),再 RUN 安装依赖,之后才 COPY 源码,这样源码改动不会触发依赖重装。
  4. 善用 .dockerignore:排除 node_modules、.git、dist 等目录,减少构建上下文大小,加快 COPY 速度。
  5. 合并 RUN 指令:多个 RUN 合并为一条(用 && 连接),减少层数。

小结

多阶段构建是 Docker 生态中最实用的特性之一。它的核心思路就是构建和运行分离——在重量级环境中编译,只把最终产物打包进轻量级运行环境。不管你是写 Go、Node.js 还是 Python,掌握多阶段构建都能显著优化你的容器化工作流。

尝试把你现有的 Dockerfile 改成多阶段版本,你会发现镜像体积的惊喜变化。

发表评论