切换语言
切换主题

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。

问题出在哪?四个字:该留的没留,该扔的没扔

具体来说:

  1. 基础镜像太大ubuntu:20.04 本身就 77MB,装完 Go 工具链直接破 900MB
  2. 编译工具残留:gcc、make、git 这些构建工具在生产环境根本用不到
  3. 缓存没清理:apt/apk 的包管理缓存全留在镜像层里
  4. 依赖冗余:开发依赖和测试框架也一起打进去了

打个比方:你出门旅游,带了行李箱、睡袋、帐篷、炊具……结果你只是去住酒店。多阶段构建就是让你只带真正需要的东西——衣服和洗漱用品,其他全扔家里。

根据 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"]

这里有几个技巧:

  1. FROM scratch:空镜像,0 字节起点,只有你的二进制文件
  2. CGO_ENABLED=0:禁用 CGO,生成纯静态二进制
  3. CA 证书:如果你的应用需要调用 HTTPS 接口,必须复制证书文件
  4. 依赖缓存优化:先复制 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"]

要点:

  1. npm ci --only=production:只安装 dependencies,跳过 devDependencies,体积立刻小一半
  2. npm cache clean --force:清理 npm 缓存,不然它会留在镜像层里
  3. 构建和运行分离: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,然后整个目录复制到生产镜像。

核心技巧:

  1. --no-cache-dir:pip 默认会缓存下载的包,加上这个参数避免缓存残留
  2. slim vs alpine:构建阶段用 slim(兼容性好),生产阶段用 alpine(体积小)
  3. 虚拟环境:如果依赖复杂,可以考虑用 venv 而不是 --user

实测数据:一个使用 FastAPI + SQLAlchemy 的项目,原始镜像约 300MB,多阶段构建后约 100MB。

基础镜像选型:Alpine vs Distroless vs Slim

选择运行阶段的基础镜像,是个需要权衡的决策。

我把三种主流方案的对比整理成一张表:

特性AlpineDistrolessSlim
基础大小3-5MB20-65MB50-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-compatapk 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-slimpython:3.9-slim)是 Alpine 和完整镜像之间的折中方案。

体积比 Alpine 大一些,但兼容性好,有 shell 可以调试。如果你不想折腾 musl/glibc 问题,slim 是省心选择。

我的建议

  • Go 应用:优先用 scratchalpine
  • 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 . . 会把整个项目目录都复制进去,包括 .gitnode_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

    步骤1: 分析当前镜像构成

    使用 `docker history` 命令查看镜像各层大小:

    ```bash
    docker history your-image:tag
    ```

    识别占用空间最大的层,通常为:
    • 基础镜像本身
    • 构建工具和编译依赖
    • 包管理缓存
  2. 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

    步骤3: 构建并对比镜像大小

    构建新镜像并对比体积变化:

    ```bash
    docker build -t myapp:optimized .
    docker images | grep myapp
    ```

    对比优化前后的大小差异。
  4. 4

    步骤4: 验证应用功能正常

    运行容器并测试应用:

    ```bash
    docker run -d -p 8080:8080 myapp:optimized
    curl http://localhost:8080/health
    ```

    确保功能完整,无依赖缺失。
  5. 5

    步骤5: 部署到生产环境

    更新 CI/CD 流程使用新镜像:

    • 推送到镜像仓库
    • 更新 Kubernetes Deployment 或 docker-compose.yml
    • 验证部署成功

常见问题

多阶段构建会影响构建速度吗?
多阶段构建会增加构建时间(因为要构建两个阶段),但最终镜像体积显著减小,部署和传输速度会大幅提升。对于 CI/CD 流程,整体耗时通常是降低的。
Alpine 和 Distroless 该如何选择?
Go 静态编译应用优先选 Alpine 或 scratch;Node.js/Python 建议先用 slim 确认兼容性,再尝试 Alpine;对安全性要求极高的场景选 Distroless,但需要提前准备日志和监控方案。
多阶段构建适用于哪些语言?
几乎所有编程语言都适用。效果最显著的是 Go(可缩减到 10MB)、Node.js(缩减 80%+)、Python(缩减 60%+)、Rust、Java 等有编译步骤或依赖管理的语言。
如何在多阶段构建中处理配置文件?
配置文件通常在运行阶段单独挂载,不建议打包进镜像。可以使用 Docker volume 或 Kubernetes ConfigMap。如果必须打包,在运行阶段 COPY 配置文件即可。
多阶段构建能减小多少镜像体积?
取决于语言和应用类型。Go 应用通常可缩减 90%-99%(从 1GB 到 10MB);Node.js 应用缩减 70%-90%;Python 应用缩减 50%-70%。关键是只保留运行必需的文件。
FROM scratch 有什么注意事项?
scratch 是空镜像,没有 shell、没有包管理器、没有 CA 证书。如果应用需要 HTTPS 调用,必须从 builder 阶段复制 /etc/ssl/certs/ca-certificates.crt。调试困难,建议先用 alpine 验证功能。

10 分钟阅读 · 发布于: 2026年4月19日 · 修改于: 2026年4月19日

相关文章

BetterLink

想持续收到这个主题的更新?

你可以直接关注作者更新、订阅 RSS,或者继续沿着系列入口往下读,避免下次又回到搜索结果重新找。

关注公众号

评论

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