uv 系列(七):CI/CD、Docker 与私有索引——生产级交付
核心目标:把本地 uv 工作流可靠迁移到 GitHub Actions 和生产容器,正确使用锁文件、缓存、私有索引和短期凭据,建立可审计的交付链路。
前置知识:已掌握 uv 项目、锁文件、构建发布和 workspace。
文档基线:uv 0.11.x;GitHub Actions、Docker 和索引行为依据 2026-07-21 的 uv 官方文档复核。CI 中应定期升级并重新验证固定版本。
7.1 生产交付的四条底线
本地执行成功不代表能够稳定交付。CI 和容器至少满足:
- 输入可追踪:代码提交、Python、uv、锁文件和基础镜像都有明确版本;
- 构建可重复:CI 不静默改锁文件,容器不复制本机
.venv; - 权限最小化:测试 job 没有发布权限,长期 token 不进入镜像和日志;
- 缓存可丢弃:删除全部缓存后仍能得到正确结果。
缓存只是一条虚线。如果移除缓存后构建失败,问题在声明或环境,而不是“缓存配置不够好”。
7.2 GitHub Actions:最小可靠工作流
.github/workflows/ci.yml:
name: ci
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Python ${{ matrix.python-version }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.12", "3.14"]
steps:
- name: Check out source
uses: actions/checkout@v7
- name: Install uv and Python
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.30"
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Verify lockfile and sync
run: uv sync --locked --all-groups --all-extras
- name: Check formatting
run: uv run --locked ruff format --check .
- name: Lint
run: uv run --locked ruff check .
- name: Type check
run: uv run --locked mypy src
- name: Test
run: uv run --locked pytest --cov --cov-report=term-missing
- name: Minimize persistent uv cache
if: always()
run: uv cache prune --ci
7.2.1 为什么固定 uv 和 Action
version: "0.11.30" 防止 Runner 某天自动切换 uv 行为。setup-uv 固定到完整提交 SHA,降低 tag 被移动带来的供应链风险。
上例为易读仍使用 actions/checkout@v7。高安全仓库应把所有第三方 Action(包括官方 Action)固定到审核过的完整 SHA,并由 Dependabot/Renovate 提交升级 PR。
7.2.2 为什么用 --locked
CI 的职责是验证仓库状态,不是替开发者生成新锁文件。若 pyproject.toml 与 uv.lock 不一致,--locked 应立即失败:
uv lock
git diff -- pyproject.toml uv.lock
在本地解决并提交,而不是在 workflow 中执行普通 uv lock 后继续。
7.2.3 Python 矩阵如何选择
库项目至少测试:
requires-python的最低支持版本;- 团队默认版本;
- 当前稳定 Python。
应用项目可以只测试实际部署版本,再额外增加升级预演。矩阵中的 3.14 是本文时点示例;复制工作流时应按项目真实支持范围调整。
7.3 拆分快速检查与完整矩阵
在每个 OS/Python 组合重复 Ruff 和 Mypy 往往没有收益。大型项目可拆为:
快速 job:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: "0.11.30"
enable-cache: true
- run: uv lock --check
- run: uv sync --locked --all-groups
- run: uv run ruff format --check .
- run: uv run ruff check .
- run: uv run mypy src
矩阵 job 只执行必要测试。最终分支保护同时要求两个 job 通过。
7.4 Workspace 的 CI
最可靠的基线是全量安装和测试:
- name: Sync all workspace members
run: uv sync --locked --all-packages --all-groups
- name: Test all members
run: uv run --all-packages pytest
- name: Build all publishable members
run: uv build --all-packages --clear --no-sources
为防止共享环境掩盖未声明依赖,还应为重要成员增加隔离 job:
- name: Test weather-core as a package target
run: |
uv sync --locked --package weather-core
uv run --package weather-core pytest packages/weather-core/tests
大仓库按变更范围优化时,必须包含反向依赖。核心库变化不能只测试核心库自身。
7.5 正确缓存 uv
setup-uv 内置缓存:
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: "0.11.30"
enable-cache: true
cache-dependency-glob: "uv.lock"
7.5.1 缓存键包含什么
通常至少包含:
- 操作系统和架构;
- uv 缓存格式相关信息;
uv.lock哈希;- 必要时包含 Python 版本或构建工具输入。
不要缓存整个 .venv 作为跨 Runner 复用策略。虚拟环境含绝对路径、解释器引用和平台二进制;缓存 uv 下载/构建产物,再用锁文件快速重建环境更稳妥。
7.5.2 uv cache prune --ci
CI 结束时:
uv cache prune --ci
它针对 CI 缓存保留更值得复用的本地构建 wheel,清理可快速重新下载的预构建 wheel 和展开的源码分发物。是否能加速取决于项目依赖,不应脱离测量机械添加。
7.5.3 Self-hosted Runner
自托管 Runner 的缓存不会随 job 销毁,可能无限增长。应:
- 为 Runner 配置明确
UV_CACHE_DIR; - 定期执行
uv cache prune; - 监控磁盘和 inode;
- 不让不同信任级别的仓库共享可写缓存;
- 严禁手工修改缓存内部文件。
7.6 发布 Job 的权限隔离
测试 job 不需要 id-token: write。发布 job 应独立,并依赖构建验证:
publish:
if: startsWith(github.ref, 'refs/tags/v')
needs: [quality, test]
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: "0.11.30"
- run: uv build --clear --no-sources
- run: uv publish --trusted-publishing always
进一步改进:
- 受保护 environment 需要审批;
- tag 版本必须等于
pyproject.toml版本; - 构建一次,验证后发布同一组不可变产物;
- 不在发布 job 临时修改版本或锁文件;
- 使用 OIDC Trusted Publishing,避免长期 PyPI token。
7.7 Docker 中安装 uv
官方提供仅包含 uv 二进制的 distroless 镜像。常见做法:
FROM python:3.12-slim-trixie
COPY --from=ghcr.io/astral-sh/uv:0.11.30 /uv /uvx /bin/
生产环境优先固定镜像 digest:
COPY --from=ghcr.io/astral-sh/uv@sha256:<审核过的摘要> /uv /uvx /bin/
不要复制文档中的示例摘要后长期不更新;应从组织信任的镜像仓库获取、验证并由自动化升级。
7.8 单包项目的生产 Dockerfile
# syntax=docker/dockerfile:1.7
ARG PYTHON_IMAGE=python:3.12-slim-trixie
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.30
FROM ${UV_IMAGE} AS uv-bin
FROM ${PYTHON_IMAGE} AS builder
COPY --from=uv-bin /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
WORKDIR /app
# 依赖层:源码变化不会使其失效
COPY pyproject.toml uv.lock README.md ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-install-project
# 项目层
COPY src ./src
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-editable
FROM ${PYTHON_IMAGE} AS runtime
RUN groupadd --system app \
&& useradd --system --gid app --home-dir /app app
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
USER app
ENTRYPOINT ["weather"]
CMD ["Shanghai"]
7.8.1 为什么分两次同步
第一次只复制依赖声明和锁文件:
uv sync --locked --no-dev --no-install-project
它安装传递依赖,不安装频繁变化的当前项目。源码复制后第二次同步安装项目。业务代码变化不会使整个依赖层失效。
7.8.2 为什么使用 UV_LINK_MODE=copy
BuildKit cache mount 与目标 .venv 可能位于不同文件系统,硬链接不可用。设置 copy 可避免链接警告,并确保最终镜像层不依赖已卸载的 cache mount。
7.8.3 为什么最终镜像不包含 uv
运行时只需要 .venv 中的 Python 和入口脚本。将 uv 留在 builder 可缩小攻击面。若生产运维确实需要 uv run,可以复制 uv,但要明确理由。
7.9 .dockerignore 不可省略
.git/
.github/
.venv/
__pycache__/
.pytest_cache/
.mypy_cache/
.ruff_cache/
dist/
build/
.env
.env.*
*.pem
*.key
.venv 必须排除:本机环境不可移植,而且可能覆盖容器刚创建的 Linux 环境。敏感文件同时应从 Git 和构建上下文排除;.dockerignore 不是秘密管理系统,只是最后一道防线。
7.10 Workspace 的 Docker 分层
早期依赖层如果只看到根 pyproject.toml,uv 无法验证锁文件是否与所有成员一致。因此官方建议:
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim-trixie AS builder
COPY --from=ghcr.io/astral-sh/uv:0.11.30 /uv /uvx /bin/
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1
WORKDIR /app
# 此阶段没有成员 pyproject.toml,跳过新鲜度检查和成员安装
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-dev --no-install-workspace
COPY . /app
# 看到完整 workspace 后必须严格校验
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-editable --package weather-cli
这里早期使用 --frozen 不是因为它更严格,而是因为缺少成员元数据,无法执行完整新鲜度检查。复制完整仓库后,最终 --locked 必须成功。
7.11 容器安全与可重复性
7.11.1 基础镜像
- 固定 Python 小版本或 digest;
- 定期重建以获取系统安全更新;
- slim 镜像可能缺编译器和共享库,构建扩展时使用 builder;
- Alpine 使用 musl,不应假设 manylinux wheel 可直接复用;
- 选择镜像前检查目标依赖是否提供对应 wheel。
7.11.2 非 root 运行
最终镜像创建专用用户并 USER app。若程序需要写目录,应明确创建并授权,而不是把整个 /app 设为 777。
7.11.3 字节码
UV_COMPILE_BYTECODE=1 可减少首次启动编译成本,但会增加构建时间和镜像体积。短生命周期 CLI 未必收益明显,Web 服务和无服务器冷启动场景应测量后决定。
7.11.4 构建秘密
私有索引凭据不要用 ARG 或 ENV 烘焙进镜像层。使用 BuildKit secret mount:
RUN --mount=type=secret,id=uv_index_password \
UV_INDEX_INTERNAL_PASSWORD="$(cat /run/secrets/uv_index_password)" \
uv sync --locked --no-dev
真实项目还需提供用户名或 credential provider。构建日志不得回显秘密。
7.12 私有索引配置
[[tool.uv.index]]
name = "internal"
url = "https://packages.example.com/simple"
explicit = true
authenticate = "always"
[tool.uv.sources]
company-weather-sdk = { index = "internal" }
7.12.1 explicit = true
只有通过 [tool.uv.sources] 显式绑定的包才能从该索引安装。这样不会因为添加私有索引,就让所有公共依赖都从私有源搜索。
7.12.2 默认 first-index
uv 默认对一个包停在第一个包含它的索引,并只在该索引的候选版本中解析。这与 pip 常见的合并候选行为不同,目的是降低 dependency confusion 风险。
不要为了“版本更新”随意启用:
uv sync --index-strategy unsafe-best-match
它会合并多个索引候选,更接近 pip,但显著扩大同名恶意包风险。优先修复索引顺序、同步代理或显式包绑定。
7.12.3 凭据环境变量
索引名 internal 对应:
$env:UV_INDEX_INTERNAL_USERNAME = "ci-user"
$env:UV_INDEX_INTERNAL_PASSWORD = "<secret>"
uv sync --locked
名称中的非字母数字会转换为下划线并大写。例如 internal-proxy 对应 UV_INDEX_INTERNAL_PROXY_PASSWORD。
authenticate = "always" 适合那些未认证请求会被重定向到公共页面、因而不会返回标准 401 的索引;它要求 uv 在请求前主动寻找凭据。
7.13 企业网络、证书与离线环境
7.13.1 代理
使用组织标准的 HTTPS_PROXY/HTTP_PROXY 配置,并确认代理不会破坏包哈希和 TLS 验证。CI Secret 中的代理凭据同样不能打印。
7.13.2 企业 CA
优先让 Runner/容器信任组织 CA。uv 支持使用平台证书存储,但不要把 --allow-insecure-host 当作长期修复;它会降低 TLS 保护。
7.13.3 离线 wheelhouse
可把审核过的 wheel 放在 flat index:
[[tool.uv.index]]
name = "offline"
url = "./wheelhouse"
format = "flat"
explicit = true
离线交付必须覆盖目标平台、Python ABI 和所有传递依赖。只在联网开发机下载一次并不等于完成离线验证。
7.14 供应链控制
7.14.1 时间冷却
[tool.uv]
exclude-newer = "7 days"
冷却期能避免立即采用刚上传的发行物,为社区和安全系统留出观察时间。它会降低更新速度,安全补丁需要例外流程。
7.14.2 SBOM
uv export --format cyclonedx1.5 --output-file sbom.json
SBOM 应与具体提交、锁文件和构建产物关联。它列出组件,不自动判断漏洞是否可利用。
7.14.3 发布证明
PyPI Trusted Publishing、容器 provenance 和签名各自解决不同问题:身份、构建来源和产物完整性。生产流程应保存:
- Git commit/tag;
- CI run ID;
uv.lock哈希;- wheel/sdist/image digest;
- SBOM 和扫描结果;
- 发布环境审批记录。
7.15 常见故障
CI 本地通过但 Runner 失败
按顺序检查:
- CI Python 是否在
requires-python范围内; - 是否提交了最新
uv.lock; - 本地是否依赖未声明的全局包;
- 目标平台是否有兼容 wheel 或编译工具;
- 私有索引和凭据是否只在本机配置;
- 删除缓存后是否仍失败。
Docker 每次都重新安装依赖
确认 COPY . /app 没有发生在依赖层之前。先复制 pyproject.toml、uv.lock 和构建元数据,再执行 --no-install-project。
容器出现跨文件系统链接警告
在 BuildKit cache mount 场景设置:
ENV UV_LINK_MODE=copy
Workspace 早期依赖层报锁文件过期
早期层缺少成员元数据,使用 --frozen --no-install-workspace;复制完整 workspace 后必须执行 --locked。
私有包解析到了公共 PyPI
使用 explicit = true 和 [tool.uv.sources] 将包绑定到命名索引,检查索引优先级,不要用 unsafe-best-match 掩盖配置问题。
私有索引持续 401/403
检查环境变量名称转换、token 权限、索引 URL 是否以 /simple 结尾、代理和 CA;需要主动认证的索引设置 authenticate = "always"。
7.16 生产验收清单
CI
- uv、Python 和第三方 Action 版本固定且有升级流程。
-
uv sync --locked在空缓存 Runner 上成功。 - 最低支持 Python 和生产 Python 都有测试。
- 测试 job 只有只读权限,发布权限位于独立受保护 job。
- 缓存键包含锁文件,删除缓存不影响正确性。
Docker
-
.dockerignore排除.venv、Git、缓存和秘密。 - 依赖层与源码层分开。
- 最终镜像以非 root 用户运行。
- 基础镜像和 uv 镜像固定版本/digest。
- 私有索引秘密通过 secret mount 提供,不进入镜像历史。
- 从最终镜像执行健康检查或 CLI 冒烟测试。
供应链
- 内部包显式绑定私有索引。
- 保持默认
first-index,例外经过安全评审。 - 发布优先使用 OIDC,没有长期 PyPI token。
- 产物、SBOM、commit 和 CI run 能互相追踪。
- 构建产物经过漏洞、许可证和秘密扫描。
7.17 本篇小结
生产级 uv 流程的重点不是“CI 里也能运行 uv sync”,而是把锁文件当作不可变输入、把缓存当作可丢弃加速层、把测试与发布权限分离,并保证 Docker 最终镜像只包含运行必需内容。私有索引的 explicit 绑定和默认 first-index 则为 Python 依赖供应链提供了重要边界。
下一篇将给出从 pip/pip-tools、Poetry、PDM 和 Pipenv 迁移的分阶段方案,并建立覆盖解释器、解析、构建、网络和缓存的系统排障方法。
官方参考
更多推荐
所有评论(0)