|
|
🎭 多阶段构建:让你的镜像体积减少90%
你是否遇到过这样的问题:
📦 "镜像怎么这么大?" —— 一个简单的 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"]
复制代码
问题:
基础镜像约 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"]
复制代码
关键语法:
:给阶段命名
:从指定阶段复制文件
━━━━━━━━━━━━━━━━━━━━
实战一: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 学习交流群,和大家一起讨论实践:

🔔 关注公众号,不错过每一篇干货! |
|