一名初级开发人员将 Python Docker 镜像从 1.2 GB 缩减到了 85 MB,使 CI 流水线的耗时从 11 分钟降至 90 秒。更小的镜像下载更快、存储成本更低,且暴露的安全漏洞也更少。

为什么镜像大小至关重要

每当拉取容器时,镜像仓库都会流式传输整个镜像。85 MB 的层可以在几秒钟内完成传输;而 1.2 GB 的层在网络状况一般的情况下可能需要几分钟。构建代理(Build agents)还必须上传并缓存完整的镜像,从而增加了 CI 时间和云存储费用。每个额外的软件包都是一个潜在的漏洞,因此精简基础镜像可以缩小攻击面。

臃肿的原因何在

  • 如果 gcc 等构建工具安装在运行应用的同一个阶段,它们会留在最终镜像中。
  • 包管理器缓存(例如 aptpip 缓存)会占用磁盘空间,且默认情况下不会被清理。
  • 每个 RUN 指令都会创建一个新的只读层;跨层重复的文件会不断累积。
  • ubuntu:latest 这样的大型基础镜像自带完整的操作系统,远超一个精简的 Python 运行时所需的内容。

三步缩减法

步骤 基础镜像 构建方法 最终大小
1 标准 Python (完整版) 单阶段构建,包含所有工具 1.18 GB
2 python:slim 多阶段构建:使用带 gcc 的 builder 阶段,最终阶段仅复制已编译的包 210 MB
3 python:alpine 在 Alpine Linux 上进行多阶段构建,其本身也非常精简 85 MB

步骤 1 – 基准线

从默认的 Python 镜像开始,开发人员得到了一个 1.18 GB 的产物。该镜像包含了完整的 Debian 栈、开发头文件和 pip 缓存。

步骤 2 – 使用 builder 的 slim 版本

切换到 python:slim 减少了操作系统的占用,但构建工具仍然存在。通过添加一个 builder 阶段,可以让 gcc、make 和其他编译时依赖项进行安装、编译,然后随后被移除。最终阶段使用 COPY --from=builder 仅提取已编译的 wheels 和运行时文件,将大小降至 210 MB。

步骤 3 – Alpine 胜出

Alpine Linux 基于 musl libc 和 busybox 构建。在 Alpine 上重复多阶段构建模式,产生了一个 85 MB 的镜像——比原始镜像减少了 93%。开发人员注意到,Kubernetes 在几秒钟内就拉取了镜像,且 CI 作业在 90 秒内便完成了。

实际影响

  • 降低存储成本 – 镜像仓库存储占用减少。
  • 提高安全性 – 软件包越少,需要追踪的 CVE 就越少。Alpine 镜像仅包含 Python 运行时和应用程序代码。
  • 更快的 CI – 流水线运行时间从 11 分钟降至 90 秒。

精简 Dockerfile 的实用技巧

  • 避免使用 :latest 标签;根据需求选择 :slim:alpine 变体。
  • 使用多阶段构建:一个专门用于编译的 builder 阶段,以及一个仅接收实际所需产物的 runtime 阶段。
  • 使用 COPY --from=builder /path/to/installed /path/in/final 进行选择性复制。
  • 优化命令顺序,使依赖项安装在复制源代码之前运行;这可以最大限度地利用层缓存。
  • 显式清理缓存,例如 rm -rf /var/lib/apt/lists/* ~/.cache/pip

如果你采用 Alpine,当应用遇到 DNS 解析问题时,请添加 nss 软件包。使用 pip 安装时,请将 /root/.local/bin 添加到 PATH 前面,以便在运行时找到本地安装的脚本。

注意事项

Alpine 的 musl libc 可能会与为 glibc 编译的二进制 wheels 发生冲突,导致运行时错误。在这种情况下,请在 Alpine builder 内部重新构建 wheels,或者退回到 slim 基础镜像。为了 DNS 的可靠性,额外添加 nss 软件包是值得的。

后续关注点

  • 扫描现有镜像,寻找可以进行多阶段重写的庞大层。
  • 监控 CI 日志中的上传和下载时间,以检测镜像臃肿问题。
  • 密切关注所选基础发行版的漏洞报告。

教训很明显:一个规范的 Dockerfile、一个轻量级的基础镜像和一个 builder 阶段可以将 Python 镜像缩小一个数量级以上,在不牺牲功能的前提下,带来切实的提速和成本效益。