Docker 多阶段构建实战:生产镜像从 1GB 瘦身到 10MB
“镜像推送失败了,超时。”
那是去年一个周五的下午,CI/CD 流水线红了一片。我盯着屏幕上那个 980MB 的 Go 应用镜像,心里咯噔一下。运维同事走过来,叹了口气:“你这镜像比我中午下的电影还大。”
后来我用了多阶段构建。
10MB。同样的应用,同样的功能,镜像体积从 980MB 缩减到 10MB。99% 的体积消失,CI/CD 推送从 3 分钟变成 3 秒。
这篇文章我会分享多阶段构建的实战技巧,包括 Go、Node.js、Python 三种语言的完整 Dockerfile 模板,以及我在踩坑过程中总结的 5 个常见错误。如果你想让自己的生产镜像从”臃肿”变”精瘦”,往下看。
为什么你的镜像这么臃肿?
说实话,大部分 Docker 镜像臃肿,原因都差不多。
我以前写过这样一个 Dockerfile:
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y golang
COPY . /app
WORKDIR /app
RUN go build -o myapp
CMD ["./myapp"]
看起来挺正常对吧?但 docker images 一看——好家伙,980MB。
问题出在哪?四个字:该留的没留,该扔的没扔。
具体来说:
- 基础镜像太大:
ubuntu:20.04本身就 77MB,装完 Go 工具链直接破 900MB - 编译工具残留:gcc、make、git 这些构建工具在生产环境根本用不到
- 缓存没清理:apt/apk 的包管理缓存全留在镜像层里
- 依赖冗余:开发依赖和测试框架也一起打进去了
打个比方:你出门旅游,带了行李箱、睡袋、帐篷、炊具……结果你只是去住酒店。多阶段构建就是让你只带真正需要的东西——衣服和洗漱用品,其他全扔家里。
根据 Docker 官方文档的数据,一个典型的 Go 应用,未优化镜像约 800MB-1GB,优化后可以压缩到 10-20MB。差距就是这么夸张。
多阶段构建的核心原理
多阶段构建的核心思想很简单:构建环境和运行环境分离。
传统的 Dockerfile 把编译、打包、运行全塞在一个镜像里。多阶段构建允许你定义多个 FROM 指令,每个 FROM 开启一个新的构建阶段。
看一个最简单的例子:
# 第一阶段:构建
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# 第二阶段:运行
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]
核心语法就两行:
FROM ... AS builder:给这个阶段起个名字COPY --from=builder:从 builder 阶段复制文件
原理上,Docker 会按顺序执行每个阶段,但最终镜像只包含最后一个阶段的内容。前面那些臃肿的编译工具、依赖缓存,全部被丢弃。
根据 iximiuz Labs 2026 年的教程,多阶段构建的本质是利用 Docker 的分层机制:每个 FROM 指令启动一个独立的构建上下文,你可以从任意阶段复制文件到后续阶段,但不相关的文件永远不会进入最终镜像。
这就好比装修房子:第一阶段是施工队,带着电钻、锤子、锯子;第二阶段是你入住,只带家具和电器。施工队走了,工具也跟着走,你的房子里只有你需要的东西。
实战案例:三语言多阶段构建模板
Go 语言:从 980MB 到 10MB
Go 是最适合多阶段构建的语言,因为它能编译成静态二进制文件。
完整 Dockerfile:
# 构建阶段
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 先复制 go.mod 和 go.sum,利用缓存
COPY go.mod go.sum ./
RUN go mod download
# 复制源码并编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o myapp .
# 运行阶段
FROM scratch
# 从 builder 复制二进制文件
COPY --from=builder /app/myapp /myapp
# 复制 CA 证书(如果需要 HTTPS 调用)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/myapp"]
这里有几个技巧:
FROM scratch:空镜像,0 字节起点,只有你的二进制文件CGO_ENABLED=0:禁用 CGO,生成纯静态二进制- CA 证书:如果你的应用需要调用 HTTPS 接口,必须复制证书文件
- 依赖缓存优化:先复制 go.mod/go.sum,再 go mod download,这样源码改动不会导致依赖重新下载
构建完成后,镜像大小约 10MB。对比原来的 980MB,缩减 99%。
如果你觉得 scratch 太极端(没有 shell,调试困难),可以用 alpine:
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
镜像会稍微大一点,约 15MB,但你有了一个可以 docker exec 进去调试的环境。
Node.js:从 900MB 到 120MB
Node.js 的多阶段构建稍微复杂一点,因为需要处理 node_modules。
完整 Dockerfile:
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
# 复制 package.json
COPY package*.json ./
# 安装所有依赖(包括 devDependencies)
RUN npm ci
# 复制源码
COPY . .
# 如果有构建步骤(如 TypeScript 编译)
RUN npm run build
# 生产阶段
FROM node:18-alpine
WORKDIR /app
# 设置 Node 环境变量
NODE_ENV=production
# 只安装生产依赖
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# 复制构建产物
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
要点:
npm ci --only=production:只安装dependencies,跳过devDependencies,体积立刻小一半npm cache clean --force:清理 npm 缓存,不然它会留在镜像层里- 构建和运行分离:TypeScript 编译在 builder 阶段完成,生产镜像只有 JS 文件
根据 Oak Oliver Engineering 的实测数据,一个典型的 Express 应用,未优化约 900MB,多阶段构建后约 120MB。缩减率约 87%。
Python:从 300MB 到 100MB
Python 的情况更特殊——它没有编译步骤,但有庞大的依赖包(numpy、pandas 动辄几百 MB)。
完整 Dockerfile:
# 构建阶段
FROM python:3.9-slim AS builder
WORKDIR /app
# 安装依赖到用户目录
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# 生产阶段
FROM python:3.9-alpine
WORKDIR /app
# 复制依赖
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
# 复制应用代码
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
这里用的是 pip install --user,把依赖安装到 /root/.local,然后整个目录复制到生产镜像。
核心技巧:
--no-cache-dir:pip 默认会缓存下载的包,加上这个参数避免缓存残留- slim vs alpine:构建阶段用
slim(兼容性好),生产阶段用alpine(体积小) - 虚拟环境:如果依赖复杂,可以考虑用 venv 而不是
--user
实测数据:一个使用 FastAPI + SQLAlchemy 的项目,原始镜像约 300MB,多阶段构建后约 100MB。
基础镜像选型:Alpine vs Distroless vs Slim
选择运行阶段的基础镜像,是个需要权衡的决策。
我把三种主流方案的对比整理成一张表:
| 特性 | Alpine | Distroless | Slim |
|---|---|---|---|
| 基础大小 | 3-5MB | 20-65MB | 50-100MB |
| 安全性 | 中等 | 极高 | 中等 |
| 调试难度 | 低(有 shell) | 高(无 shell) | 低(有 shell) |
| 兼容性 | 有坑(glibc) | 好 | 好 |
| 适用场景 | Go 静态二进制 | 高安全要求 | Node.js/Python |
Alpine:体积最小,但要小心 glibc
Alpine Linux 使用 musl libc 而不是标准的 glibc。这对 Go 来说没问题(因为可以静态编译),但对 Python、Node.js 的一些依赖可能有问题。
我踩过这个坑:一个 Python 项目用了 numpy,在 Alpine 里跑不起来,报错 ImportError: cannot import name 'random'。查了一圈才发现是 musl 和 glibc 的兼容问题。
解决方案有两个:
- 安装
libc6-compat:apk add libc6-compat - 或者干脆用
slim代替alpine
Distroless:安全标杆,但调试麻烦
Distroless 是 Google 出的镜像系列,特点是没有 shell、没有包管理器、只有运行应用必需的东西。
根据 danieldemmel.me 的分析,Distroless 可以消除大部分高危 CVE 漏洞,因为攻击者没法通过 shell 执行命令。
但代价是:出问题时没法 docker exec 进去看日志、调试。你只能依赖日志输出和监控。
如果你追求极致安全,Distroless 是最佳选择:
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/myapp /
ENTRYPOINT ["/myapp"]
Slim:平衡选择
官方的 -slim 镜像(如 node:18-slim、python:3.9-slim)是 Alpine 和完整镜像之间的折中方案。
体积比 Alpine 大一些,但兼容性好,有 shell 可以调试。如果你不想折腾 musl/glibc 问题,slim 是省心选择。
我的建议:
- Go 应用:优先用
scratch或alpine - Node.js/Python:先用
slim,确认没问题再尝试alpine - 高安全要求:用
distroless,但提前准备好调试方案
避坑指南:5 个常见错误与解决方案
写了这么多 Dockerfile,我踩过的坑大概能填满一个游泳池。挑 5 个最常见的说一说。
错误 1:COPY —from=0 全量复制
新手容易犯的错误:直接从前一阶段全量复制。
# 错误示范
FROM builder
COPY --from=0 /app /app
这会把 builder 阶段的整个目录都复制过来,包括 Go 工具链、npm 缓存、临时文件……镜像立刻膨胀。
正确做法:只复制需要的文件。
# 正确示范
COPY --from=builder /app/myapp /myapp
COPY --from=builder /app/dist /dist
错误 2:缓存没清理
apt/apk 的包管理缓存会留在镜像层里,即使你删掉了。
# 错误示范(缓存会留在上一层)
RUN apt-get update && apt-get install -y curl
RUN apt-get clean
正确做法:清理命令和安装命令在同一层执行。
# 正确示范
RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/*
或者用 --no-cache 参数:
RUN apk add --no-cache curl
错误 3:Alpine glibc 兼容问题
前面说过,Alpine 用 musl libc,部分 Python/Node.js 依赖不兼容。
典型错误:
ImportError: cannot import name 'random' from 'numpy.random'
解决方案:要么装 libc6-compat,要么换 slim。
错误 4:未设置非 root 用户
默认情况下,容器以 root 用户运行,安全风险比较大。
最佳实践:创建专用用户。
RUN adduser -D appuser
USER appuser
这样即使容器被攻击,攻击者也只有普通用户权限。
错误 5:忽略了 .dockerignore
.dockerignore 是 Dockerfile 的”减法清单”。如果不配置,COPY . . 会把整个项目目录都复制进去,包括 .git、node_modules、测试文件……
创建 .dockerignore:
.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
*.md
.env
这能减少构建上下文大小,加快镜像构建速度。
结论
多阶段构建是 Docker 镜像瘦身最实用的技巧。
核心思想就一句话:构建环境留编译工具,运行环境只放应用。
回顾一下数据:
- Go:980MB → 10MB(缩减 99%)
- Node.js:900MB → 120MB(缩减 87%)
- Python:300MB → 100MB(缩减 67%)
如果你还没用过多阶段构建,现在可以试试。找一个项目,对照上面的模板改写 Dockerfile,然后用 docker images 对比前后大小。
相信你会看到惊喜——至少 CI/CD 推送不会再超时了。
Docker 多阶段构建镜像优化
将 Docker 镜像从臃肿状态精简到最小体积的完整流程
⏱️ 预计耗时: 30 分钟
- 1
步骤1: 分析当前镜像构成
使用 `docker history` 命令查看镜像各层大小:
```bash
docker history your-image:tag
```
识别占用空间最大的层,通常为:
• 基础镜像本身
• 构建工具和编译依赖
• 包管理缓存 - 2
步骤2: 编写多阶段 Dockerfile
创建包含构建阶段和运行阶段的 Dockerfile:
```dockerfile
# 构建阶段
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .
# 运行阶段
FROM alpine:3.18
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
```
关键点:
• 使用 AS 给阶段命名
• COPY --from=builder 只复制必要文件 - 3
步骤3: 构建并对比镜像大小
构建新镜像并对比体积变化:
```bash
docker build -t myapp:optimized .
docker images | grep myapp
```
对比优化前后的大小差异。 - 4
步骤4: 验证应用功能正常
运行容器并测试应用:
```bash
docker run -d -p 8080:8080 myapp:optimized
curl http://localhost:8080/health
```
确保功能完整,无依赖缺失。 - 5
步骤5: 部署到生产环境
更新 CI/CD 流程使用新镜像:
• 推送到镜像仓库
• 更新 Kubernetes Deployment 或 docker-compose.yml
• 验证部署成功
常见问题
多阶段构建会影响构建速度吗?
Alpine 和 Distroless 该如何选择?
多阶段构建适用于哪些语言?
如何在多阶段构建中处理配置文件?
多阶段构建能减小多少镜像体积?
FROM scratch 有什么注意事项?
10 分钟阅读 · 发布于: 2026年4月19日 · 修改于: 2026年4月19日
相关文章
Dockerfile入门教程:从零构建你的第一个Docker镜像(含实例)
Dockerfile入门教程:从零构建你的第一个Docker镜像(含实例)
Docker vs 虚拟机:5分钟搞懂性能差异与场景选择指南
Docker vs 虚拟机:5分钟搞懂性能差异与场景选择指南
Docker安装避坑指南2025:从permission denied到成功运行的完整解决方案

评论
使用 GitHub 账号登录后即可评论