资料库 · 打包、依赖与容器化
第 ④ 卷,条目 61–80。这一卷几乎全部来自 pythonspeed.com(Itamar Turner-Trauring)。
为什么集中在一个来源:Python 打包这块的知识过期速度惊人——pip 的行为、PEP 668 的破坏性变化、uv 的崛起、基础镜像的最佳选择,两年前的文章现在照做可能就是错的。这个站的特点是同一个作者持续维护、文章标日期和版本、并且会回头更新(比如"最佳基础镜像"那篇标着 2026 年 2 月)。这种可追溯性比拼十个来源的碎片更可靠。
这一卷是可以照着做的一卷,和前三卷的性质不同。
基础镜像与镜像体积
61. The best Docker base image for your Python application (February 2026)
来源:pythonspeed.com,标注 2026 年 2 月更新。
读它解决什么:FROM 后面到底该写什么,这是这个问题最直接的答案。
要点:
- 针对 Python 应用的基础镜像选择建议,明确标注时间为 2026 年 2 月
- 标题带日期,说明作者认为这个结论有时效性
我的补充:标题带日期是这篇最重要的特征,说明作者自己承认这个答案会过期。我的默认选择是 python:3.x.y-slim(Debian slim):有 glibc、能装预编译 wheel、体积可接受。不选 Alpine 的原因见第 64 条。不选完整版 python:3.x.y 是因为它带着编译工具链和一堆你运行时不需要的东西,几百 MB 的攻击面白送。注意版本号一定写全(3.14.7 而不是 3.14),否则构建不可复现。
62. A deep dive into the "official" Docker image for Python
来源:pythonspeed.com。
读它解决什么:想知道 python:3.x 官方镜像里到底装了什么、怎么构建的。
要点:
- 深入分析 Docker Hub 上官方 Python 镜像的构成
- 标题给 "official" 加引号,暗示这个"官方"名分值得推敲
我的补充:引号是有讲究的——这些镜像由 Docker 的社区维护者维护,不是 Python 核心团队发布的。知道这点对合规审查有用(有些组织要求依赖必须来自上游官方)。技术上值得知道的是:官方镜像里的 Python 是从源码编译的,编译选项和你的发行版包管理器装的 Python 不同(比如是否开 PGO/LTO),所以性能可能有差异。另外它把 Python 装在 /usr/local 而不是 /usr,这会影响你在镜像里手写路径时的判断。
63. Broken by default: why you should avoid most Dockerfile examples
来源:pythonspeed.com。
读它解决什么:网上抄来的 Dockerfile 为什么大多有问题。
要点:
- 论点是大多数流传的 Dockerfile 示例默认就是坏的
- "Broken by default" 直接给出立场
我的补充:我同意这个判断,也能补出最常见的几个坏味道:用 root 运行(没有 USER 指令)、COPY . . 在装依赖之前(任何代码改动都让依赖层缓存失效,见第 66 条)、pip install 不带 --no-cache-dir(把 pip 缓存留在镜像层里白占体积)、FROM python:latest(构建不可复现)。教程 Dockerfile 的目标是"五行内跑起来",生产 Dockerfile 的目标是"快、小、安全、可复现",这两个目标基本不重叠。
64. Using Alpine can make Python Docker builds 50x slower
来源:pythonspeed.com。
读它解决什么:为什么"用 Alpine 减小镜像体积"这个建议对 Python 项目通常是错的。
要点:
- Alpine 会让 Python 的 Docker 构建慢到 50 倍
- 标题给出了具体的量级
我的补充:根因是 musl libc 与 manylinux wheel 的不兼容。PyPI 上的预编译 wheel 按 manylinux 标准构建,依赖 glibc;Alpine 用 musl,于是 pip 认为没有可用 wheel,退回到从源码编译。你装 numpy、pandas、lxml、cryptography 时,那不是下载 30 秒的事,是编译十几分钟、还得先装一整套编译工具链(于是镜像反而更大)。现在有 musllinux wheel 标准了,覆盖面在改善,但远不如 manylinux 完整。结论:Python 项目默认用 slim 而不是 alpine,除非你的依赖全是纯 Python 且确实需要极小体积。这个坑我见过太多人踩,通常表现为"CI 突然变得要跑二十分钟"。
65. Shrinking your Python application's Docker image: an overview
来源:pythonspeed.com。
读它解决什么:镜像太大,想知道有哪些正交的减小手段。
要点:
- 系统梳理减小 Python 镜像体积的各类方法
- 定位是 overview,是这个主题的入口
我的补充:先想清楚为什么要小,再决定投入多少。小镜像的真实收益主要是拉取速度(尤其是自动扩容时新节点要拉镜像)和攻击面,而不是磁盘钱。而且如果你的层设计得好,增量拉取时只有变化的层需要传输——一个 800 MB 但依赖层稳定的镜像,日常部署实际传输量可能比 200 MB 但每次全变的镜像更小。所以层的稳定性比总体积更值得优化。真要压体积,多阶段构建(第 71–73 条)的收益最大,其他手段都是零碎的。
构建缓存与构建速度
66. Faster or slower: the basics of Docker build caching
来源:pythonspeed.com。
读它解决什么:搞清 Docker 的层缓存什么时候命中、什么时候失效。
要点:
- Docker 构建缓存的基本机制
- 标题的 "faster or slower" 表明同一机制既能帮你也能坑你
我的补充:一条规则能解决 80% 的问题:按变化频率从低到高排列 Dockerfile 的指令。依赖清单(requirements.txt/pyproject.toml)变得少,先 COPY 它并装依赖;应用代码变得勤,最后才 COPY。写反了的后果是每改一个字符就重装全部依赖。正确骨架:
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .另一个容易忽略的点:.dockerignore 缺失会让 COPY . . 把 .git、__pycache__、node_modules 全塞进去——既让缓存无谓失效(.git 每次提交都变),又白涨体积。
67. The high cost of slow Docker builds
来源:pythonspeed.com。
读它解决什么:给"花时间优化构建速度"这件事找到成本论证。
要点:
- 论述缓慢的 Docker 构建带来的实际代价
- 是动机层面的文章,不是技术手册
我的补充:真实代价不在 CI 的机器费用上,在反馈回路上。构建 3 分钟你会一天部署好几次;构建 25 分钟你会攒着改动一起发,于是每次发布的风险面变大、出问题时二分定位更难、回滚也更慢。这是个自我强化的恶性循环。如果要说服人投入,别拿 CI 账单说事(那点钱说服力不够),拿"上次故障从发现到回滚用了多久"说事。
68. Speeding up Docker builds in CI with BuildKit
来源:pythonspeed.com。
读它解决什么:CI 里每次都是干净环境,缓存全丢,BuildKit 怎么解决。
要点:
- 用 BuildKit 加速 CI 中的 Docker 构建
- CI 环境无本地缓存是这个问题的前提
我的补充:CI 里的关键机制是 --cache-from / --cache-to:把层缓存导出到远端镜像仓库,下次构建从那儿拉。GitHub Actions 上用 type=gha,其他地方用 type=registry。有个反直觉的现象要提醒:如果缓存本身很大,拉缓存的时间可能超过重新构建的时间。所以只对"构建慢、缓存小"的层做缓存最划算——依赖层符合这个特征,编译大型 C 扩展的层往往不符合。上线前测一下两种情况的真实耗时再决定。
69. Speed up pip downloads in Docker with BuildKit's new caching
来源:pythonspeed.com。
读它解决什么:想在 Docker 构建里复用 pip 的下载缓存。
要点:
- 用 BuildKit 的缓存挂载来加速 pip 下载
- 与传统的层缓存是不同的机制
我的补充:这里的机制是 RUN --mount=type=cache,和层缓存互补:
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt区别在于——层缓存是"整层复用或整层重建",只要 requirements.txt 改一个字符整层就废;缓存挂载是持久目录,即使层要重建,已下载的包还在,只需下载新增的那个。这两个一起用效果最好。注意缓存挂载的内容不进最终镜像,所以也不用担心 --no-cache-dir 那套体积问题。
70. Faster pip installs: caching, bytecode compilation, and uv
来源:pythonspeed.com。
读它解决什么:pip 安装慢的几个不同原因,以及 uv 能改善多少。
要点:
- 从缓存、字节码编译、以及
uv三个角度谈安装加速 - 把
uv与传统 pip 放在一起比较
我的补充:容易被忽略的是字节码编译(.pyc 生成)这一项——它在 CI 里可能占安装时间的相当一部分。取舍是:构建时编译(--compile)会让构建慢、镜像大,但容器启动快;构建时不编译,则第一次导入模块时才编译,冷启动变慢。对短生命周期的容器(Serverless、频繁扩缩容)应该在构建时编译;对长跑的服务无所谓。uv 这块的收益是真实的(Rust 写的解析器 + 并行下载 + 硬链接复用),迁移成本也低,它能读 requirements.txt。这也是 第一卷第 18 条 说的"热点下沉到 Rust"的又一个例子。
多阶段构建
71. Multi-stage builds #1: Smaller images for compiled code
来源:pythonspeed.com,多阶段构建系列第一篇。
读它解决什么:需要编译的依赖让镜像变大,多阶段构建怎么解决。
要点:
- 多阶段构建系列的第一篇,主题是给编译型代码减小镜像
- 系列结构说明这个主题需要分三篇讲
我的补充:核心思路是把构建期需要和运行期需要分开:第一阶段装 gcc、python3-dev、各种 -dev 头文件,编译出 wheel;第二阶段从干净的 slim 镜像开始,只 COPY --from=builder 拿编译产物。最终镜像里没有编译器,体积能砍掉几百 MB,攻击面也小得多。这是本卷里收益最大的单项技术,如果只能做一件事就做这个。
72. Multi-stage builds #2: Python specifics
来源:pythonspeed.com,系列第二篇。
读它解决什么:多阶段构建用在 Python 上有哪些语言特有的坑。
要点:
- 多阶段构建的 Python 特定注意事项
- 承接第一篇的通用原理
我的补充:Python 特有的麻烦在于**"编译产物"不是一个文件**,而是散落在 site-packages 里的一堆东西,还可能带 .so 和数据文件。两种可行做法:一是在 builder 阶段用 pip wheel 把所有依赖打成 wheel,运行阶段 pip install --no-index --find-links=/wheels;二是整个 virtualenv 直接 COPY --from,但要保证两个阶段的 Python 版本和路径完全一致,否则 venv 里记录的解释器路径会失效。第二种更简单,前提是两阶段用同一个基础镜像的 builder/slim 变体。
73. Multi-stage builds #3: Speeding up your builds
来源:pythonspeed.com,系列第三篇。
读它解决什么:多阶段构建反而变慢了,怎么让它同时又小又快。
要点:
- 多阶段构建的速度优化,系列收尾
- 隐含前提:多阶段有可能拖慢构建
我的补充:多阶段和缓存的冲突点在于——builder 阶段的产物不在最终镜像的层里,所以 --cache-from 只拉最终镜像时,builder 阶段的缓存全丢,每次都要重编译。解法是把 builder 阶段也导出成缓存(--target builder 单独构建并推送,或用 BuildKit 的 mode=max 导出全部中间层)。这个细节很多人漏掉,表现是"本地构建很快,CI 上每次都重新编译 numpy"。
74. Elegantly activating a virtualenv in a Dockerfile
来源:pythonspeed.com。
读它解决什么:Dockerfile 里 source venv/bin/activate 不起作用,正确做法是什么。
要点:
- 在 Dockerfile 中激活 virtualenv 的正确方式
- 隐含的问题是常规 activate 脚本在 Dockerfile 里行为不符合预期
我的补充:根因是每条 RUN 都是独立的 shell,前一条 RUN source activate 设的环境变量到下一条就没了。正确做法是设环境变量而不是跑脚本:
ENV VIRTUAL_ENV=/opt/venv
RUN python -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"ENV 是持久的,之后所有 RUN、CMD、ENTRYPOINT 都能拿到。顺带一提:容器里到底要不要用 venv 有争论(容器本身已经是隔离了),我倾向于用,因为它让多阶段构建里"把依赖整体搬走"变成一次 COPY。
安全与密钥
75. Don't leak your Docker image's build secrets
来源:pythonspeed.com。
读它解决什么:构建时需要私有仓库凭据,怎么用而不留在镜像里。
要点:
- 讲如何避免构建密钥泄露到镜像中
- 前提是构建过程确实需要访问密钥
我的补充:最要紧的认知是 RUN rm secret.txt 删不掉它。镜像是分层的,前一层写进去的文件在那一层里永久存在,任何人 docker save 之后都能翻出来;--build-arg 传的值也会留在镜像元数据里,docker history 直接可见。唯一正确的方式是 BuildKit 的 secret 挂载:
RUN --mount=type=secret,id=pipconf,target=/etc/pip.conf \
pip install -r requirements.txt它挂成临时文件,不进任何层。这条和这个博客的经历直接相关:仓库的 git 历史里躺着两个已泄露的高德/百度 API key,我把它们从源码里移除了,但历史里还在,所以必须去控制台轮换——同一个道理,"删掉"从来不等于"没泄露过"。
76. How to (not) use Docker to share your password with hackers
来源:pythonspeed.com。
读它解决什么:用具体的错误示范讲清密钥泄露的几种路径。
要点:
- 以反讽标题讲 Docker 使用中的密码泄露
- 与上一条互补,偏向展示错误做法
我的补充:补两条最容易中的:一是 COPY . . 把 .env 带进镜像——本地开发的 .env 里往往是真凭据,.dockerignore 必须包含它;二是多阶段构建的 builder 阶段拿了密钥,以为不进最终镜像就安全——如果你把 builder 阶段也推到仓库当缓存(第 73 条那个做法),密钥就跟着上去了。这两个组合起来是真实事故的常见形态:为了优化 CI 缓存,无意间把带密钥的中间层公开了。
77. Finding leaked secrets in your Docker image with a scanner
来源:pythonspeed.com。
读它解决什么:想主动扫描自己的镜像里有没有已经泄露的密钥。
要点:
- 用扫描器检测镜像中泄露的密钥
- 属于事后检测手段,与前两条的预防互补
我的补充:扫描要逐层扫,只扫最终文件系统会漏掉"写进去又删掉"的那类(第 75 条)。这类工具应该放在 CI 的镜像构建之后、推送之前,扫到就阻断推送。同时也在代码提交侧加一道(pre-commit 钩子跑密钥扫描),因为镜像扫描发现问题时,密钥往往已经在 git 历史里了——那时候补救成本高得多,只能轮换。两道都设,成本都不高。
78. The security scanner that cried wolf
来源:pythonspeed.com。
读它解决什么:安全扫描器报出几百个漏洞,怎么判断哪些真的要管。
要点:
- 论述安全扫描器的误报问题
- "cried wolf" 指反复虚报导致告警失去意义
我的补充:这条很重要,因为扫描器噪音会直接毁掉扫描的价值。典型情形:扫 python:slim 镜像报出 200 个 CVE,其中绝大多数在你的场景里根本不可触发——某个 CVE 在 libxml2 里,而你的应用从不解析不可信 XML;或者漏洞在一个装了但从未被 import 的包里。团队第一次看到 200 条会认真查,第三次就开始无脑忽略,于是真正该管的那条也被忽略了。可行做法:区分"可达性"(有工具能分析漏洞代码路径是否真被调用),对确认不适用的建立带过期时间的例外清单(强制定期复核,而不是永久忽略),并且只把 critical/high 且已知可利用的设为阻断级别。
79. Building on solid ground: reproducible Docker builds for Python
来源:pythonspeed.com。
读它解决什么:同一份 Dockerfile 构建两次结果不同,怎么做到可复现。
要点:
- Python 项目的可复现 Docker 构建
- 前提是默认情况下 Docker 构建并不可复现
我的补充:不可复现的来源有四层,都得堵住:基础镜像的浮动 tag(写全版本,或者更严格地用 digest python:3.14.7-slim@sha256:...)、未锁定的 Python 依赖(用锁文件,见 第一卷第 15–16 条)、apt-get install 拿到的系统包版本(发行版仓库里的包会更新)、构建时的时间戳。为什么值得做:可复现是排障能力。线上出问题,你要能构建出跟当时一模一样的镜像来复现——做不到的话,"在我这儿好的"就成了永远无法澄清的争论。
80. Should you use uv's managed Python in production?
来源:pythonspeed.com。
读它解决什么:uv 能自己下载管理 Python 解释器,生产环境该不该用。
要点:
- 讨论生产环境是否该用
uv托管的 Python 解释器 - 标题以问句形式提出,说明答案有条件
我的补充:这里要区分两件事:用 uv 做依赖解析和安装(我认为基本可以无脑用,快且兼容),和用 uv 托管的 Python 解释器本体(需要斟酌)。后者的考虑点:那些解释器是第三方构建的(python-build-standalone 那套),编译选项和官方镜像不同,对 dlopen 系统库的行为可能有差异;安全更新的时效也依赖那个项目的节奏而非上游。我的取舍是——CI 和开发环境用 uv python 很方便(能快速切多个版本测矩阵),生产镜像仍然用官方 python:x.y.z-slim 作为解释器来源,只用 uv 装依赖。两边的收益都拿到了,风险留在了不影响线上的那一侧。
本卷小结
三个带走的结论:
- Python 项目别用 Alpine。 musl 让 manylinux wheel 全部失效,退回源码编译,构建时间能差 50 倍,镜像反而更大。默认
slim。 - 删不掉的是层,不是文件。
RUN rm和--build-arg都留痕,唯一正确的构建期密钥方案是 BuildKit 的 secret 挂载。这也是为什么"从源码里删掉 key"不等于安全,必须轮换。 - 按变化频率排 Dockerfile。 依赖清单先 COPY 并安装,应用代码最后 COPY。这一条加上多阶段构建,覆盖了绝大部分构建速度与体积问题。
上一卷: ← ③ Rust 与系统编程 · 下一卷: ⑤ 软件设计与重构 →
