Python 基础体系 · 第 111/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。
Python 容器与 CI:镜像、依赖缓存、测试、制品和供应链
Python 应用进入 CI/CD 后,真正需要交付的不是“源码加一个启动命令”,而是一组可以被验证、复现、追踪和回滚的对象:
源码提交
│
├─ 依赖解析与锁定
├─ 测试与静态检查
├─ 构建 wheel / sdist
├─ 构建容器镜像
├─ 生成 SBOM、摘要和证明材料
└─ 发布、部署、验证、回滚
这里有三个经常被混为一谈的概念:
- 缓存:为了更快地重新生成结果,可以丢失,丢失后应能重新下载或构建。
- 制品:某次构建产生的、需要被保存、传递或部署的结果,例如 wheel、sdist、测试报告和镜像。
- 供应链证据:说明制品由什么源码、什么构建环境、什么依赖和什么流程生成的材料。
一个可靠的流水线必须保证:缓存失效不会改变结果;制品不会被后续任务偷偷重新构建;供应链证据能够指向具体的源码提交和制品摘要。
一、先区分 Python 包、应用依赖和容器镜像
1. Python 包的两个身份
Python 生态中,“包”至少有两个容易混淆的身份:
- import package:运行时被
import的目录或模块。 - distribution package:通过索引下载、安装和发布的分发包。
例如:
项目名称:weather-service
导入名称:weather
分发文件:
weather_service-1.4.0-py3-none-any.whl
weather_service-1.4.0.tar.gz
weather 是导入路径,weather-service 是项目名称。二者可以相同,也可以不同。
CI 中构建和发布的是 distribution package;容器运行时使用的是安装后的 import package。不要因为本地能执行:
python -m weather
就认为项目已经具备可发布性。发布还需要元数据、版本、依赖声明、许可证文件和正确的分发格式。
2. wheel 与 sdist 在容器中的不同作用
wheel 是安装分发格式,文件扩展名为 .whl,本质上是具有规定名称和目录结构的 ZIP 压缩包。它通常包含已经准备好的 Python 文件、扩展模块和 .dist-info 元数据,安装时不需要重新执行项目构建流程。(packaging.python.org)
sdist 是 source distribution,即源代码分发包。现代 sdist 是 .tar.gz,包含 pyproject.toml、PKG-INFO 和构建所需的源文件;安装者可能需要从它重新构建 wheel。(packaging.python.org)
这一区别直接影响容器构建:
安装 wheel:
下载 wheel → 校验 → 解包到 site-packages
安装 sdist:
下载 sdist → 解包 → 安装构建依赖 → 构建 wheel → 安装 wheel
因此,生产镜像通常优先安装已经在 CI 中构建并测试过的 wheel,而不是在最终运行镜像中执行:
pip install .
后者可能在部署阶段重新编译 C 扩展、重新解析依赖,导致“测试过的对象”和“实际运行的对象”并不相同。
二、依赖声明、锁定和安装不是同一件事
1. pyproject.toml 描述允许范围
一个最小项目可以这样声明:
[build-system]
requires = ["setuptools>=75"]
build-backend = "setuptools.build_meta"
[project]
name = "weather-service"
version = "1.4.0"
requires-python = ">=3.14"
dependencies = [
"httpx>=0.28,<0.29",
]
[project.optional-dependencies]
test = [
"pytest>=8,<9",
]
其中:
requires-python = ">=3.14"
表示项目支持的 Python 范围;
httpx>=0.28,<0.29
表示依赖允许的版本范围;
version = "1.4.0"
表示当前项目分发版本。
这些声明解决的是兼容性约束,不是某次安装必须得到的完整版本集合。例如 httpx>=0.28,<0.29 仍然可能解析出不同的补丁版本。
2. 锁文件描述某次可复现选择
锁文件记录的是一次解析后的结果,通常包括:
- 包名;
- 精确版本;
- 适用的 Python 和平台条件;
- wheel 或 sdist 文件;
- 文件大小;
- SHA-256 等哈希;
- 某些情况下的来源和构建证明。
PyPA 当前的 pylock.toml 规范定义了标准化的锁文件格式。它要求锁文件使用 pylock.toml 或符合规定模式的名称,并允许记录不同环境、依赖组、wheel、sdist 及其哈希。(packaging.python.org)
可以把依赖安装抽象成一个函数:
其中:
- :项目依赖声明,例如
pyproject.toml; - :锁定结果;
- :目标解释器和平台,例如 CPython 3.14、Linux x86_64;
- :安装模式,例如生产依赖或测试依赖;
- :最终环境。
只有 而没有 时,安装器还需要进行版本选择:
R 是解析过程,而 index state 可能随时间变化。即使源码没有变化,索引中新发布的兼容版本也可能改变解析结果。
如果存在锁定结果,安装过程更接近:
这时安装器应先判断当前环境是否满足锁文件中的 requires-python 和环境标记,再选择适配当前环境的 wheel,并校验文件大小和哈希。(packaging.python.org)
3. 反例:只固定顶层依赖
下面的文件并没有完整锁定环境:
httpx==0.28.1
因为 httpx 的传递依赖仍可能使用范围约束:
httpcore>=1.0.0,<2
anyio
certifi
idna
如果这些传递依赖没有被同时锁定,两个时间不同的 CI 运行可能得到不同环境。
另一个常见反例是:
RUN pip install -r requirements.txt
但 requirements.txt 中只有:
httpx>=0.28
这只是把范围约束放进了另一个文件,并没有自动产生可复现安装。
三、Docker 镜像是分层文件系统,不是压缩后的虚拟机
Docker 镜像通常由多个只读层组成,每条主要构建指令会形成或参与形成一个层;容器启动时再叠加一个可写层。层是前序文件系统变化的增量,后层可以覆盖前层的文件。(docs.docker.com)
因此,镜像构建有两个不同目标:
- 构建速度:尽可能复用以前的层。
- 运行镜像体积和风险:不要把编译器、缓存、源码和凭据带入最终镜像。
这两个目标并不总是相同。把所有东西压缩成一个层,可能减少某些重复文件,但会损失层共享和并行拉取能力;Docker 文档也指出,多阶段构建通常比简单压缩层更适合控制最终镜像内容。(docs.docker.com)
1. 缓存失效的因果链
假设 Dockerfile 为:
FROM python:3.14-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "-m", "weather"]
源码中任何一个文件发生变化,COPY . . 对应的层就可能失效;该层之后的 pip install 也会重新执行。Docker 的缓存规则是:某一层失效后,依赖该层的后续层也需要重新构建。(docs.docker.com)
更合理的顺序是先复制低频变化的依赖输入,再复制高频变化的源码:
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
COPY pyproject.toml ./
COPY pylock.toml ./
RUN python -m pip install --upgrade pip \
&& python -m pip install --no-cache-dir .
COPY src ./src
CMD ["python", "-m", "weather"]
但这个示例还有一个重要前提:项目必须已经具备可构建的 pyproject.toml,并且安装命令使用的依赖输入确实完整。如果项目源码参与构建元数据计算,例如动态版本、自动生成代码或构建期读取源码,那么只复制 pyproject.toml 可能不足以完成构建。
更稳妥的生产模式是多阶段构建:在 builder 阶段构建 wheel,在 runtime 阶段只安装 wheel。
四、用多阶段构建隔离编译环境和运行环境
下面的结构适合一个使用 src 布局的 Python 3.14 应用:
# syntax=docker/dockerfile:1
FROM python:3.14-slim AS builder
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /build
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY pyproject.toml ./
COPY src ./src
RUN python -m pip install --upgrade pip build \
&& python -m build --wheel --outdir /dist \
&& python -m pip install --no-deps /dist/*.whl
FROM python:3.14-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
RUN useradd --create-home --uid 10001 appuser
USER appuser
CMD ["python", "-m", "weather"]
执行过程分为四步:
builder创建虚拟环境;python -m build --wheel生成 wheel;pip install --no-deps /dist/*.whl安装当前项目;runtime只复制安装后的虚拟环境,不复制构建工具和源码。
这里的 --no-deps 不是“忽略依赖”,而是避免在第二次安装项目时重新解析依赖。实际依赖应在 builder 阶段由锁文件或明确的依赖安装步骤完成。若没有安装依赖,应用启动时会出现:
ModuleNotFoundError: No module named 'httpx'
所以完整版本可以改为:
FROM python:3.14-slim AS builder
ENV PATH="/opt/venv/bin:$PATH" \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /build
RUN python -m venv /opt/venv
COPY pyproject.toml ./
COPY pylock.toml ./
RUN python -m pip install --upgrade pip
# 具体锁文件安装命令取决于所选安装器。
# 如果使用 requirements.txt 导出物,应由 CI 生成并校验该文件。
COPY requirements.lock.txt ./
RUN python -m pip install --require-hashes \
-r requirements.lock.txt
COPY src ./src
RUN python -m pip install --no-deps \
--no-cache-dir \
--no-index \
--find-links=/dist \
. || python -m pip install --no-deps .
RUN python -m build --wheel --outdir /dist
实际工程中不应把“失败后换一种安装方式”的命令直接作为默认方案,因为它会掩盖错误来源。更清晰的流程是:
CI 先生成并验证 wheel
↓
镜像构建阶段复制 wheel
↓
runtime 阶段 --no-deps 安装该 wheel
例如:
FROM python:3.14-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY dist/weather_service-1.4.0-py3-none-any.whl /tmp/app.whl
RUN python -m pip install \
--no-cache-dir \
--no-deps \
/tmp/app.whl \
&& rm /tmp/app.whl
CMD ["python", "-m", "weather"]
这样,容器中的应用分发包就是 CI 产出的那个文件,而不是镜像构建时重新生成的另一个文件。
五、依赖缓存有三层,不能互相替代
Python CI 中至少存在三类缓存:
1. 索引或代理缓存
保存从包索引下载的 wheel / sdist
2. 包管理器下载缓存
例如 pip 的 HTTP 和 wheel 缓存
3. Docker BuildKit 缓存
保存 Dockerfile 指令对应的构建层
它们的失效条件不同:
| 缓存 | 主要保存对象 | 典型失效条件 |
|---|---|---|
| 索引代理缓存 | 包文件和索引响应 | TTL、清理策略、上游变化 |
| pip 缓存 | 下载文件和构建结果 | 缓存目录被清理、缓存键变化 |
| Docker 层缓存 | 指令产生的文件系统层 | 指令或输入文件变化 |
| CI 缓存 | runner 上指定路径 | 缓存键变化、淘汰、权限范围变化 |
1. Docker 层缓存
Docker 文档建议将不常变化的依赖文件提前复制,将频繁变化的源码放到后面;这样修改源码时不会触发依赖安装层重建。(docs.docker.com)
COPY pyproject.toml pylock.toml ./
RUN python -m pip install ...
COPY src ./src
缓存键的逻辑可以表示为:
其中 是哈希函数。如果 src/app.py 改变,而依赖输入没有改变,则依赖安装层的输入仍然可以保持不变。
2. BuildKit cache mount
层缓存失效后,包管理器仍可能重新下载全部依赖。BuildKit 的 cache mount 可以把包管理器缓存目录持久化到构建缓存中:
# syntax=docker/dockerfile:1
FROM python:3.14-slim
WORKDIR /app
COPY requirements.lock.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install -r requirements.lock.txt
这里要区分:
RUN层缓存命中:整个安装步骤不执行;- cache mount 命中:安装步骤执行,但已有包文件可以从缓存目录复用。
因此,层缓存命中通常更快;cache mount 则在层缓存失效时降低重新下载成本。Docker 官方文档说明,cache mount 是跨构建累积的持久缓存位置,尤其适合包管理器下载目录。(docs.docker.com)
3. CI 依赖缓存
以 GitHub Actions 为例,依赖缓存通常按键恢复:先查找精确键,再查找部分匹配或恢复键;缓存内容不能原地修改,新的内容需要使用新的键保存。(docs.github.com)
一个通用的缓存键应至少包含:
操作系统
Python 版本
架构
安装器或包管理器版本
锁文件哈希
形式化表示:
如果只使用:
python-dependencies
那么 Python 3.13、Python 3.14、Linux 和 Windows 可能错误共享同一个缓存。
缓存只能加速流程,不能成为正确性的前提。每个 job 都必须能够在缓存完全为空时重新下载依赖并成功完成。
六、缓存与制品必须严格分开
缓存和制品的区别可以用一个判定问题表达:
删除它之后,系统应该“重新生成”,还是应该“找回原对象”?
- pip 下载目录删除:重新下载即可,这是缓存。
- Docker 构建缓存删除:重新构建即可,这是缓存。
weather_service-1.4.0-py3-none-any.whl删除:需要找回本次构建产物,这是制品。- 测试报告删除:可能影响审计和故障分析,这是应保存的构建输出。
GitHub 文档也明确区分了两者:缓存用于跨运行复用依赖和中间结果,任务应能在缓存不存在时重新生成;artifact 用于保存构建输出、日志或在不同 job 之间传递文件。(docs.github.com)
CI 中不要这样做:
job build:
构建 wheel
job image:
不下载 build job 的 wheel
再执行 python -m build
这会产生两个不同的构建事件:
wheel₁:build job 生成
wheel₂:image job 重新生成
即使它们文件名相同,内容也可能不同。正确的数据流是:
build job
├─ dist/*.whl
└─ dist/*.tar.gz
│
└─ artifact upload
│
▼
image job
├─ artifact download
├─ 校验 SHA-256
└─ COPY wheel 到镜像
七、测试的对象不是“源码”,而是交付路径
Python 项目至少需要测试四种状态:
- 源码工作树:测试开发环境中的导入和行为;
- 安装后的环境:验证包元数据、依赖和入口点;
- 构建出的 wheel:验证分发包内容;
- 最终容器:验证运行时文件、用户、启动命令和配置。
只测试源码存在一个典型漏洞:
src/weather/__init__.py
在源码目录中可以被导入,但构建 wheel 时可能因为包发现配置错误而没有被打包。
1. 构建并检查分发包
可以使用 PyPA 生态的构建工具:
python3.14 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip build twine
python -m build
find dist -maxdepth 1 -type f -print
python -m twine check dist/*
预期结果类似:
dist/weather_service-1.4.0-py3-none-any.whl
dist/weather_service-1.4.0.tar.gz
Checking dist/weather_service-1.4.0-py3-none-any.whl: PASSED
Checking dist/weather_service-1.4.0.tar.gz: PASSED
wheel 文件名中的兼容性标签包含 Python、ABI 和平台信息,例如:
weather_service-1.4.0-cp314-cp314-manylinux_2_17_x86_64.whl
其中:
cp314:CPython 3.14;- 第二个
cp314:对应 ABI; manylinux_2_17_x86_64:目标平台;py3-none-any:纯 Python、无特定 ABI、跨平台。
wheel 规范规定了这种文件名结构,并要求 .dist-info 中至少包含 METADATA、WHEEL 和 RECORD;RECORD 记录安装文件及其安全哈希。(packaging.python.org)
2. 从 wheel 安装后再测试
rm -rf /tmp/test-env
python3.14 -m venv /tmp/test-env
. /tmp/test-env/bin/activate
python -m pip install dist/*.whl
python -c "import weather; print(weather.__file__)"
python -m pytest -q
如果项目自身的测试依赖没有打进生产 wheel,就应额外安装测试组:
python -m pip install ".[test]"
python -m pytest -q
更严格的测试方式是将测试代码放在项目外部,确保测试不会意外从当前工作树导入源码:
mkdir -p /tmp/package-check
cp -r tests /tmp/package-check/
cd /tmp/package-check
python -m pytest -q
3. 容器测试
镜像构建完成后,至少应验证:
docker build -t weather-service:test .
docker run --rm weather-service:test \
python -c "import weather; print('import ok')"
如果服务使用 HTTP 接口,则运行容器并执行健康检查:
docker run -d --rm \
--name weather-test \
-p 18080:8080 \
weather-service:test
curl --fail http://127.0.0.1:18080/health
docker logs weather-test
docker stop weather-test
这一步验证的是最终镜像,而不是 builder 阶段的虚拟环境。常见失败包括:
ModuleNotFoundError
说明运行镜像缺少依赖;
PermissionError
说明切换到非 root 用户后,应用需要写入的目录没有权限;
Connection refused
说明服务没有监听预期端口,或启动命令、绑定地址与容器编排配置不一致。
八、一个可审计的 CI 阶段划分
一个不依赖具体 CI 平台的流水线可以拆为:
flowchart LR
A[提交源码] --> B[依赖解析与锁定校验]
B --> C[静态检查与单元测试]
C --> D[构建 wheel / sdist]
D --> E[安装 wheel 后测试]
E --> F[上传 Python 制品]
F --> G[下载同一制品]
G --> H[构建容器镜像]
H --> I[容器启动与集成测试]
I --> J[生成 SBOM 与证明材料]
J --> K[签名并推送镜像]
K --> L[部署与运行时验证]
每个阶段的输入和输出应明确:
| 阶段 | 输入 | 输出 |
|---|---|---|
| 依赖解析 | pyproject.toml、锁文件 |
可安装依赖集合 |
| 测试 | 源码、依赖环境 | 测试报告 |
| 构建 | 源码、构建后端 | wheel、sdist |
| 包测试 | wheel、测试依赖 | 安装验证结果 |
| 镜像构建 | 基础镜像、wheel | 镜像 manifest digest |
| 安全检查 | 镜像、依赖元数据 | SBOM、漏洞报告 |
| 发布 | 已验证镜像 | registry 中的不可变摘要 |
| 部署 | 镜像摘要、配置 | 运行中的版本 |
关键点是:下游 job 应下载上游产生的 wheel,而不是重新执行构建;部署应引用镜像 digest,而不是仅引用可变标签。
不稳定引用:
weather-service:latest
稳定引用:
weather-service@sha256:<manifest-digest>
标签适合人类查找,digest 适合机器确认对象身份。
九、供应链安全:从“能安装”到“能证明”
1. 哈希验证证明了什么
如果依赖文件中写有:
httpx==0.28.1 \
--hash=sha256:<expected-hash>
安装器可以计算下载文件的 SHA-256:
只有:
安装才继续。
这能防止下载内容在传输或镜像代理过程中被替换,但不能单独证明:
- 包作者是谁;
- 构建环境是否可信;
- 源码是否来自预期提交;
- 包中是否存在恶意代码。
哈希解决的是对象完整性,不是完整的来源证明。
2. wheel 内部的 RECORD 也不是签名
wheel 的 RECORD 为文件提供哈希记录,安装器可以据此检查压缩包中的文件是否被修改。规范要求除 RECORD 本身外,文件应记录安全哈希,且不允许使用 MD5 或 SHA-1;安装时如果文件缺失、未记录或哈希不匹配,安装应失败。(packaging.python.org)
但 RECORD 位于 wheel 内部。如果攻击者可以整体替换 wheel 和其中的 RECORD,内部哈希无法证明 wheel 来自谁。因此还需要外部信任机制,例如:
- HTTPS 和可信索引;
- 私有索引访问控制;
- 包文件哈希锁定;
- 发布签名;
- CI 身份证明;
- 构建 provenance;
- 镜像签名和部署侧验证。
3. provenance 与 SBOM
SBOM 是 Software Bill of Materials,即软件物料清单,描述镜像或分发包中包含哪些组件。
provenance 是构建来源证明,描述制品由哪个源码、哪个工作流、什么构建参数生成。
PyPA 的索引托管证明规范定义了与发布者、工作流和证明对象关联的数据结构;pylock.toml 也允许记录包文件对应的证明身份。(packaging.python.org)
它们解决的问题不同:
SBOM:
“这个镜像里有什么?”
provenance:
“这个镜像是怎么来的?”
签名:
“谁为这个对象的身份作保证?”
漏洞扫描:
“其中的组件是否匹配已知风险?”
不要把漏洞扫描结果当作安全证明。扫描通过只说明当前规则集没有发现问题,不代表制品来源可信,也不代表没有未知漏洞。
十、基础镜像和依赖镜像的双重信任边界
容器供应链至少包含两条链:
基础镜像链:
Python 运行时 → Debian/Alpine 基础层 → 系统库
Python 依赖链:
PyPI/私有索引 → wheel/sdist → 安装环境
因此,锁定 Python 依赖但使用浮动基础镜像,仍然不能得到完全可复现的运行环境:
FROM python:3.14-slim
这个标签可能在未来指向不同的基础镜像构建结果。生产发布更适合使用经过验证的 digest:
FROM python:3.14-slim@sha256:<verified-digest>
这里的 digest 应由组织的镜像更新流程维护,而不是手工从不明来源复制。更新基础镜像时,应重新执行:
- 镜像构建;
- 包安装验证;
- 单元和集成测试;
- SBOM 生成;
- 漏洞扫描;
- 灰度部署。
基础镜像更新不是单纯的“换一个标签”,因为它可能改变 OpenSSL、glibc、系统 CA 证书、动态链接库和系统用户行为。
十一、CI 缓存的攻击面
缓存不仅影响速度,也可能影响安全。
以共享 CI 缓存为例,如果低信任分支可以写入一个高信任工作流随后读取的缓存,就可能出现缓存投毒:
不可信 Pull Request
↓ 写入伪造缓存
可信主分支工作流
↓ 恢复缓存并执行其中脚本
潜在代码执行或凭据泄露
GitHub 文档特别提醒,不应把令牌、凭据等敏感信息放入缓存;恢复出的缓存内容应视为不可信输入,缓存投毒可能导致代码执行。(docs.github.com)
具体到 Python,应避免缓存:
~/.pypirc
带 token 的 pip 配置
私有索引凭据文件
云平台认证文件
包含密钥的构建目录
如果必须访问私有索引,应优先使用 CI 平台的短期凭据和临时配置,并确保日志不会打印完整命令行参数:
python -m pip install \
--index-url "https://${PIP_USER}:${PIP_TOKEN}@packages.example.invalid/simple" \
-r requirements.lock.txt
更安全的实现通常是将认证写入短生命周期配置文件,并在步骤结束后删除,而不是将令牌长期保存在缓存目录中。
十二、故障诊断应先判断对象身份,再判断构建逻辑
1. “本地可以,CI 不可以”
先比较以下信息:
python --version
python -m pip --version
python -c "import sys, platform; print(sys.version); print(platform.platform())"
python -m pip debug --verbose
重点不是只看 Python 主版本,还要看:
- CPython 还是其他实现;
- x86_64 还是 ARM64;
- glibc 还是 musl;
- wheel 是否存在匹配标签;
- 是否退回到 sdist;
- 构建工具版本是否不同。
如果目标环境没有匹配 wheel,安装器可能选择 sdist 并进入本地编译路径。此时错误通常表现为:
error: command 'gcc' failed
fatal error: Python.h: No such file or directory
这不是“pip 随机失败”,而是当前平台没有可直接安装的二进制分发包,或构建环境缺少编译依赖。
2. “镜像很慢,但缓存看起来存在”
分别检查三件事:
docker buildx du
docker history weather-service:test
python -m pip cache dir
如果 Docker 层命中但 pip 仍然下载,说明可能是:
- 安装步骤的层被源码复制顺序破坏;
- pip 缓存没有挂载到构建阶段;
- CI runner 没有恢复 BuildKit 外部缓存;
- 锁文件变化导致正确失效;
- 使用了不同的 Python、平台或安装器版本。
Docker 官方文档指出,CI 环境通常缺少跨运行持久化,因此外部 BuildKit 缓存对减少镜像构建时间尤其重要;外部缓存可以通过 registry 等后端导出和导入。(docs.docker.com)
3. “测试通过,但生产启动失败”
这种故障通常发生在测试对象不完整:
测试对象:源码工作树
生产对象:最终镜像
诊断顺序应是:
docker inspect weather-service:test
docker run --rm weather-service:test python -m pip list
docker run --rm weather-service:test python -c "import weather"
docker run --rm weather-service:test id
分别确认:
- 启动命令是什么;
- 依赖是否真的安装;
- 导入路径是否存在;
- 当前用户是否符合预期。
十三、与生产交付的连接:镜像只是版本载体
镜像解决的是文件系统和运行时依赖的一致性,但不自动解决生产运行问题。
同一个镜像仍然需要明确:
- 进程模型:单进程、多个 worker,还是由平台管理副本;
- 容量模型:CPU、内存、连接数和队列长度;
- 配置注入:环境变量、文件、密钥管理;
- 数据库迁移:先扩展兼容,再切换应用,再收缩旧结构;
- 灰度发布:先让少量流量进入新镜像;
- 回滚:回到旧镜像 digest,而不是重新构建旧源码。
尤其要区分代码回滚和数据库回滚。代码可以通过镜像 digest 快速切换,但数据库迁移可能已经改变持久化状态。因此生产发布应尽量采用向后兼容的迁移顺序:
1. 增加兼容字段或表
2. 部署能同时读写新旧结构的应用
3. 回填数据
4. 切换读取路径
5. 删除旧结构
如果第一步和最后一步被合并成不可逆迁移,那么应用镜像即使能回滚,旧版本也可能无法读取新数据库。
十四、一个可执行的最小验收流程
在本地或 CI 中,可以按以下顺序验收:
set -eux
python3.14 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip build
python -m build
python -m pip install ".[test]"
python -m pytest -q
python -m twine check dist/*
sha256sum dist/*
docker build \
--tag weather-service:test \
.
docker run --rm weather-service:test \
python -c "import weather; print('package import ok')"
这个流程的逻辑是:
- 用明确的 Python 3.14 创建隔离环境;
- 构建 wheel 和 sdist;
- 在安装环境中运行测试;
- 检查分发包元数据;
- 保存制品摘要;
- 构建最终镜像;
- 在最终镜像中再次验证导入。
如果生产需要严格可复现,则还应把以下对象固定并保存:
源码提交 SHA
pyproject.toml
锁文件
构建后端版本
Python 3.14 基础镜像 digest
wheel SHA-256
sdist SHA-256
容器镜像 manifest digest
SBOM
provenance
签名或验证结果
测试报告
最终,Python 容器与 CI 的核心不是“写一个 Dockerfile,再加几条测试命令”,而是建立一条可追踪的数据流:
源码
→ 依赖选择
→ 已验证的 wheel / sdist
→ 已验证的容器镜像
→ 带摘要和证明的发布对象
→ 按 digest 部署的运行实例
当缓存失效时,流程应该只是变慢;当某个制品损坏时,流程应该拒绝发布;当生产出现问题时,系统应该能够根据镜像 digest、wheel 哈希和源码提交准确定位并回滚。
系列导航与关联阅读
- 系列入口:Python 完整学习路线:从语言模型、并发到 Web、数据、AI 与生产交付
- 上一篇:Python 实验可复现:随机种子、环境、数据、制品和运行记录
- 下一篇:Python 生产交付:进程模型、容量、配置、迁移、灰度和回滚
- 延伸:Python 包构建与发布:wheel、sdist、索引、签名和版本
官方资料
本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论