Docker 多阶段构建实战:把镜像从 1.1GB 压到 90MB 的五个手法

举报
云上小栈 发表于 2026/11/03 13:27:19 2026/11/03
【摘要】 一、问题的起点 一个很常见的场景:一个 Go 或 Node 项目,直接 docker build 出来的镜像 1.1GB,推送到镜像仓库要几分钟,拉取更慢,节点磁盘很快被吃满。但真正运行时需要的产物,可能只有几十兆。 多阶段构建(multi-stage build)就是解决这个问题的标准手段:构建环境和运行环境解耦,构建依赖全部留在中间层,最终镜像只保留运行所需。 二、手法一:用多阶段把构...

一、问题的起点

一个很常见的场景:一个 Go 或 Node 项目,直接 docker build 出来的镜像 1.1GB,推送到镜像仓库要几分钟,拉取更慢,节点磁盘很快被吃满。但真正运行时需要的产物,可能只有几十兆。

多阶段构建(multi-stage build)就是解决这个问题的标准手段:构建环境和运行环境解耦,构建依赖全部留在中间层,最终镜像只保留运行所需。

二、手法一:用多阶段把构建工具链剥离

以 Go 为例,单阶段写法的 Dockerfile 会把整个 Go SDK(约 800MB)打进镜像。CGO_ENABLED=0 编出的静态二进制根本不依赖 libc,所以完全可以只拷二进制:

FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app .

FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/app /app
ENTRYPOINT ["/app"]

注意 -ldflags="-s -w":去掉符号表和调试信息,二进制通常能再小 20%~30%,代价是 panic 堆栈里的函数名会变少,需要权衡。

三、手法二:依赖安装与源码分离,保住缓存

把 go mod download 或 npm ci 放在 COPY . . 之前,是为了让依赖层能命中缓存。只改业务代码时,依赖层不会失效,构建时间从几分钟降到十几秒。

Node 项目里这条尤其关键,并且要记得装完就清缓存:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["node","server.js"]

四、手法三:基础镜像换精简版

ubuntu:22.04 约 77MB,debian:bookworm-slim 约 75MB,alpine 约 7MB,distroless static 约 2MB。差距是数量级的。

但 Alpine 用的是 musl libc,遇到需要 glibc 的预编译二进制(部分 Python 包、部分 Java 工具)会直接报错,这时候别硬上,退回 debian-slim 更省事。distroless 更极端:没有 shell、没有包管理器,安全面最小,但进容器 docker exec -it sh 也进不去,排查问题要靠日志和指标。

五、手法四:合并 RUN,顺手清理

每一条 RUN 都会产生一个层。层里的东西后续即使删掉,体积仍记在镜像里(除非用多阶段或 --squash)。所以安装和清理必须在同一条 RUN:

RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates tzdata \
 && rm -rf /var/lib/apt/lists/*

--no-install-recommends 能省掉大量推荐包,配合 rm -rf /var/lib/apt/lists/*,通常能少 30MB 以上。

六、手法五:.dockerignore 和 BuildKit 缓存挂载

很多人忽略了 .dockerignore。COPY . . 会把 .git、node_modules、本地构建产物一起塞进上下文,既拖慢构建又污染镜像。至少要有:

.git
node_modules
dist
*.log
.env

如果用的是 BuildKit,还可以把包管理器缓存挂进来,加速而不留痕迹:

RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/go/pkg/mod \
    go build -o /out/app .

七、效果对比

同一个 Go 服务,各阶段体积变化:

单阶段 golang:1.22 基础 → 1.12GB
剥离工具链 + distroless → 96MB
加 -ldflags="-s -w" → 78MB
换静态编译 + .dockerignore 收紧 → 约 72MB

推送时间从 3 分多钟降到十几秒,节点拉取几乎瞬间完成。

八、几个容易踩的坑

1. COPY --from 只能引用前面已定义的阶段名,顺序不能反。
2. distroless 镜像没有 /bin/sh,ENTRYPOINT 必须用 exec 数组形式,不能写 shell 形式。
3. 时区问题:精简镜像默认 UTC,需要 ENV TZ=Asia/Shanghai 并确保 tzdata 存在。
4. 多阶段并不会自动让镜像变小——如果把构建阶段的整个目录 COPY --from=builder / /,等于白做。

九、小结

多阶段构建的核心思路只有一句话:构建需要的东西,运行不需要。按这个思路做减法,配合精简基础镜像、缓存分层和 .dockerignore,绝大多数业务镜像都能压到原来的十分之一以内。

镜像小带来的收益是连锁的:推送快、拉取快、扩容快、攻击面小、存储成本低。这是投入产出比最高的容器优化之一。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。