Docker 多阶段构建实战:把镜像体积从 1.2GB 压到 80MB

举报
云栈探索者 发表于 2026/10/22 14:06:38 2026/10/22
【摘要】 多阶段构建把编译工具隔离在临时阶段,只把产物拷进最终镜像。本文用 dive 逐层定位问题,给出完整 Dockerfile、buildx 跨架构构建、CI 缓存顺序与 Trivy 扫描门禁的落地做法,镜像从 1.24GB 降至 78MB,拉取时间从 3 分 21 秒降到 19 秒。

## 一、引子:一个被镜像体积拖垮的下午上周我们把一套 Python 机器学习服务推上线,CI 流水线跑得很顺,但第一次 docker push 时镜像大小是 1.24GB。K8s 拉取一次要 3 分多钟,回滚一次更久,成本账单里还多出一笔流量费。问题出在最朴素的 Dockerfile 上:编译阶段需要 gcc、CMake 和一堆 Python 头文件,运行时却只需要一个 slim 基础镜像加上一份 wheel。我们却把两套依赖装进了同一个镜像层。这篇文章记录我们如何用多阶段构建(multi-stage build)把镜像从 1.24GB 压到 78MB,全过程可复现。## 二、定位问题:先看每一层到底装了什么不要凭直觉猜,先量。我用的是 dive 这个工具来逐层分析镜像:bashdive your-app:latest输出的关键几行:Layer  Size  Description 42MB  FROM python:3.11-slim 613MB apt-get install -y build-essential cmake libgl1 380MB pip install -r requirements.txt 201MB rm -rf /var/lib/apt/lists/*问题一目了然:build-essential 加 cmake 占了 613MB,rm -rf 那一层虽然把包列表删了,但不会减少前面层的体积——分层存储的特性决定了删除操作只是在后续层加了个删除标记。这里踩过的坑值得强调:很多人以为 RUN apt-get clean 能省空间,实际上除非该层之前没有任何文件被写入,否则压缩效果非常有限。## 三、第一步:拆成两个阶段多阶段构建的本质是:让编译工具存在于临时阶段,只把编译产物拷贝到最终阶段。dockerfile# ===== 阶段一:builder =====FROM python:3.11-slim AS builderENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1RUN apt-get update && apt-get install -y --no-install-recommends build-essential cmake libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*WORKDIR /buildCOPY requirements.txt ./RUN pip install --prefix=/install -r requirements.txtCOPY . .RUN pip wheel --no-deps --wheel-dir=/wheels -e .# ===== 阶段二:final =====FROM python:3.11-slim AS finalENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/install/bin:$PATHRUN apt-get update && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*COPY --from=builder /install /usr/localWORKDIR /appCOPY --from=builder /build/app /app/appUSER appEXPOSE 8080CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8080", "app.main:app"]几个关键细节:第一,最终阶段仍然需要装运行时动态库。我们的项目依赖 opencv,它需要 libgl1 和 libglib2.0-0。这层只有几十 MB,省不掉也不能省——省掉就是 ImportError: libGL.so.1 cannot open shared object file。第二,--prefix=/install 加上 COPY --from 是关键手法。它把 site-packages 装到一个独立目录,最后整体拷贝过去,避免把 pip 自身、缓存等垃圾带进最终镜像。第三,USER app 不是可选项。容器默认以 root 运行,一旦有漏洞被利用,攻击面会扩大到宿主机。我们额外创建了非 root 用户。## 四、第二步:验证体积收益bashdocker build -t app:multistage .docker images | grep appapp:multistage   78.4MBapp:old          1.24GB体积下降 93.7%。但真正让我们意外的是拉取时间:从 3 分 21 秒降到 19 秒,K8s 滚动更新时 Pod 启动时间从平均 74 秒降到 21 秒。## 五、第三步:用 buildx 拿到跨架构镜像我们服务要同时跑在 x86 和 ARM 机器上。用 buildx 一条命令出两个架构:bashdocker buildx build --platform linux/amd64,linux/arm64 --target final -t registry.example.com/app:v1.3.0 --push .注意 --target final 这个参数。如果不指定,buildx 会构建 Dockerfile 里最后一个阶段,这恰好是我们想要的,但显式写出来更清晰,也能避免以后有人在末尾追加新阶段时踩坑。## 六、最容易被忽略的缓存问题CI 里我们把构建时间从 6 分降到 2 分 40 秒,关键在于 COPY 的顺序。错误示范:任何代码改动都会让依赖层缓存失效COPY . .RUN pip install -r requirements.txt正确:先复制依赖清单,代码变更不影响依赖层COPY requirements.txt ./RUN pip install -r requirements.txtCOPY . .这里有个真实教训:我们有一次为了优化把这两个步骤合并回一起,当时构建快了 3 秒,但接下来三个月每次改一行 Python 代码都要重装全部依赖,CI 时间反而涨到了 14 分钟。## 七、安全扫描:别只顾着体积体积优化很容易做过头,把一些东西顺手删掉了。比如我们曾经把整个 site-packages 目录 COPY 过去,结果把 pip、setuptools 也带进去了——这些工具本身没有漏洞,但会扩大攻击面。我们用 Trivy 做镜像扫描,并纳入 CI 门禁:bashtrivy image --severity HIGH,CRITICAL --exit-code 1 app:multistage另外建议在最终阶段加一行:dockerfileRUN rm -rf /root/.cache /tmp/*## 八、写在最后多阶段构建不是银弹,它有明确的适用边界。适合:编译型依赖(gcc、Rust、Go)、前端构建(node_modules 巨大)、需要下载大体积 SDK 的场景。不适合:单一阶段的简单脚本镜像,强行拆分反而增加构建复杂度。我们最终把这个流程固化到了 CI 模板里,并把镜像大小做成了门禁项——超过 200MB 就让流水线失败。半年后的今天,镜像从 1.24GB 变成 78MB,CI 省下约 40 小时,K8s 滚动更新期间的服务中断从分钟级降到秒级。这些数字比镜像本身更让我觉得这个改动值得。如果你也在为镜像体积头疼,建议先从 dive 看起。凭直觉猜往往猜不准,看一眼每一层,答案就在那儿。参考:Docker 官方文档 Multi-stage builds;dive 项目 https://github.com/wagoodman/dive

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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