共计 4172 个字符,预计需要花费 11 分钟才能阅读完成。
文章目录[显示]
🎭 多阶段构建:让你的镜像体积减少 90%
Docker 30 天实战系列 · Day 10
你是否遇到过这样的问题:
- 📦 " 镜像怎么这么大?" —— 一个简单的 Go 程序,镜像却有 1GB
- 🐌 " 部署太慢了 " —— 镜像拉取要好几分钟
- 💸 " 存储费用太高 " —— 镜像仓库空间告急
今天,我们将学习 Docker 多阶段构建(Multi-stage Build),这是优化镜像体积的 终极武器。
本文你将学到
- ✅ 理解多阶段构建的原理和优势
- ✅ 掌握多阶段构建的语法和技巧
- ✅ 实战:将 1.2GB 镜像优化到 15MB
- ✅ 学会不同语言的多阶段构建最佳实践
阅读时间:约 15 分钟
实操时间:约 25 分钟
难度等级:⭐⭐⭐⭐☆
前置准备
| 项目 | 要求 |
|---|---|
| Docker | 20.10+ |
| 前置知识 | Day 8-9 Dockerfile 基础 |
# 创建工作目录
mkdir -p ~/docker-practice/day10
cd ~/docker-practice/day10
为什么需要多阶段构建?
传统构建的问题
以一个 Go 应用为例,传统 Dockerfile:
# 传统方式:单阶段构建
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o main .
CMD ["./main"]
问题:
- 基础镜像
golang:1.21约 800MB - 包含编译器、工具链等运行时不需要的东西
- 最终镜像可能超过 1GB
# 构建后查看大小
docker images myapp
# REPOSITORY TAG SIZE
# myapp latest 1.2GB ← 太大了!
多阶段构建的思路
┌─────────────────────────────────────────────────────────┐
│ 多阶段构建原理 │
├─────────────────────────────────────────────────────────┤
│ │
│ 阶段 1:构建阶段 (Builder) │
│ ┌─────────────────────────────────┐ │
│ │ FROM golang:1.21 │ ← 包含编译工具 │
│ │ 编译代码 → 生成可执行文件 │ │
│ └─────────────────────────────────┘ │
│ │ │
│ │ 只复制编译产物 │
│ ▼ │
│ 阶段 2:运行阶段 (Runtime) │
│ ┌─────────────────────────────────┐ │
│ │ FROM alpine:3.18 │ ← 最小运行环境 │
│ │ 只包含可执行文件 │ │
│ └─────────────────────────────────┘ │
│ │
│ 最终镜像:只有阶段 2 的内容!│
└─────────────────────────────────────────────────────────┘
多阶段构建语法
基本语法
# 阶段 1:构建阶段(命名为 builder)FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o main .
# 阶段 2:运行阶段
FROM alpine:3.18
WORKDIR /app
# 从 builder 阶段复制编译产物
COPY --from=builder /app/main .
CMD ["./main"]
关键语法:
AS builder:给阶段命名COPY --from=builder:从指定阶段复制文件
实战一:Go 应用多阶段构建
步骤 1:创建示例应用
# 创建 main.go
cat > main.go << 'EOF'
package main
import (
"fmt"
"net/http"
)
func main() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello from Docker Multi-stage Build!")
})
fmt.Println("Server starting on :8080...")
http.ListenAndServe(":8080", nil)
}
EOF
# 创建 go.mod
cat > go.mod << 'EOF'
module myapp
go 1.21
EOF
步骤 2:创建多阶段 Dockerfile
cat > Dockerfile << 'EOF'
# ============ 阶段 1:构建 ============
FROM golang:1.21-alpine AS builder
# 设置工作目录
WORKDIR /app
# 复制依赖文件
COPY go.mod ./
# 下载依赖(如果有)RUN go mod download
# 复制源码
COPY . .
# 编译(静态链接,禁用 CGO)RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o main .
# ============ 阶段 2:运行 ============
FROM alpine:3.18
# 安装 CA 证书(HTTPS 请求需要)RUN apk --no-cache add ca-certificates
WORKDIR /app
# 从构建阶段复制可执行文件
COPY --from=builder /app/main .
# 暴露端口
EXPOSE 8080
# 运行
CMD ["./main"]
EOF
步骤 3:构建并对比
# 构建多阶段镜像
docker build -t myapp:multistage .
# 查看镜像大小
docker images myapp
对比结果:
| 构建方式 | 镜像大小 | 减少比例 |
|---|---|---|
| 单阶段 (golang:1.21) | ~1.2GB | - |
| 多阶段 (alpine) | ~15MB | 98.7% |
步骤 4:验证运行
# 运行容器
docker run -d -p 8080:8080 --name myapp myapp:multistage
# 测试
curl http://localhost:8080
# Hello from Docker Multi-stage Build!
# 清理
docker rm -f myapp
实战二:Node.js 应用多阶段构建
Node.js 应用的多阶段构建稍有不同,因为需要保留 node_modules。
创建示例应用
mkdir -p nodejs-app && cd nodejs-app
# package.json
cat > package.json << 'EOF'
{
"name": "nodejs-app",
"version": "1.0.0",
"main": "app.js",
"scripts": {"start": "node app.js"},
"dependencies": {"express": "^4.18.2"}
}
EOF
# app.js
cat > app.js << 'EOF'
const express = require('express');
const app = express();
app.get('/', (req, res) => {res.json({ message: 'Hello from Node.js Multi-stage Build!'});
});
app.listen(3000, () => {console.log('Server running on port 3000');
});
EOF
多阶段 Dockerfile
# ============ 阶段 1:安装依赖 ============
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
# 只安装生产依赖
RUN npm ci --only=production
# ============ 阶段 2:构建(如果有构建步骤)============
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# RUN npm run build # 如果有构建步骤
# ============ 阶段 3:运行 ============
FROM node:20-alpine AS runner
WORKDIR /app
# 创建非 root 用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nodeuser
# 从 deps 阶段复制生产依赖
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/app.js ./
COPY --from=builder /app/package.json ./
USER nodeuser
EXPOSE 3000
CMD ["node", "app.js"]
对比结果:
| 构建方式 | 镜像大小 |
|---|---|
| 单阶段 (node:20) | ~1.1GB |
| 多阶段 (node:20-alpine) | ~180MB |
实战三:前端应用多阶段构建
React/Vue 等前端应用的构建模式:
# ============ 阶段 1:构建 ============
FROM node:20-alpine AS builder
WORKDIR /app
# 安装依赖
COPY package*.json ./
RUN npm ci
# 构建
COPY . .
RUN npm run build
# ============ 阶段 2:Nginx 服务 ============
FROM nginx:alpine
# 复制构建产物到 Nginx
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制自定义 Nginx 配置(可选)# COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
对比结果:
| 构建方式 | 镜像大小 |
|---|---|
| 包含 node_modules | ~1.5GB |
| 多阶段 (nginx:alpine) | ~25MB |
高级技巧
技巧 1:使用 scratch 镜像(最小化)
对于静态编译的 Go 程序:
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o main .
# scratch 是空镜像,只有 0 字节
FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
结果:镜像只有几 MB!
技巧 2:从外部镜像复制
# 从官方镜像复制二进制文件
COPY --from=nginx:alpine /usr/sbin/nginx /usr/sbin/nginx
技巧 3:多个构建目标
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
# 开发阶段
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# 生产阶段
FROM base AS production
RUN npm ci --only=production
COPY . .
CMD ["npm", "start"]
构建指定阶段:
# 构建开发镜像
docker build --target development -t myapp:dev .
# 构建生产镜像
docker build --target production -t myapp:prod .
技巧 4:使用 BuildKit 缓存
# syntax=docker/dockerfile:1.4
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 使用缓存挂载加速依赖下载
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o main .
优化对比总结
| 语言 / 框架 | 单阶段 | 多阶段 | 减少 |
|---|---|---|---|
| Go | 1.2GB | 15MB | 98% |
| Node.js | 1.1GB | 180MB | 84% |
| React | 1.5GB | 25MB | 98% |
| Java (Spring) | 700MB | 200MB | 71% |
| Python | 1GB | 150MB | 85% |
🤔 常见问题
Q1:多阶段构建会增加构建时间吗?
A:首次构建可能稍慢,但由于层缓存,后续构建通常更快。而且部署时间大幅减少。
Q2:如何调试多阶段构建?
A:
# 只构建到指定阶段
docker build --target builder -t myapp:debug .
# 进入容器调试
docker run -it myapp:debug sh
Q3:COPY --from 可以用阶段索引吗?
A:可以,但不推荐:
# 用索引(不推荐)COPY --from=0 /app/main .
# 用名称(推荐)COPY --from=builder /app/main .
📚 本文总结
核心要点
-
多阶段构建原理:
- 使用多个 FROM 指令
- 只保留最后阶段的内容
- 通过 COPY --from 传递产物
-
关键语法:
FROM image AS name COPY --from=name /src /dest -
最佳实践:
- 构建阶段用完整镜像
- 运行阶段用最小镜像(alpine/scratch)
- 静态编译消除运行时依赖
-
适用场景:
- 编译型语言(Go、Rust、C++)
- 需要构建的前端应用
- 任何需要优化镜像大小的场景
下一步
明天我们将学习:Day 11 - 基础镜像选择指南:Alpine vs Ubuntu vs Distroless
你将了解不同基础镜像的特点,学会为项目选择最合适的基础镜像。
💬 互动时间
今日作业:
- 用多阶段构建优化你的一个项目
- 对比优化前后的镜像大小
- 尝试使用 scratch 镜像
在评论区分享:
- 你的镜像优化了多少?
- 遇到了什么问题?
🐳 加入 Docker 学习群
扫码加入 Docker 学习交流群,和大家一起讨论实践:

🔔 关注公众号,不错过每一篇干货!
正文完
创作不易,扫码加点动力
发表至: Docker
近一天内