uv 系列(七):CI/CD、Docker 与私有索引——生产级交付

uv 系列(七):CI/CD、Docker 与私有索引——生产级交付 核心目标把本地 uv 工作流可靠迁移到 GitHub Actions 和生产容器正确使用锁文件、缓存、私有索引和短期凭据建立可审计的交付链路。前置知识已掌握 uv 项目、锁文件、构建发布和 workspace。文档基线uv 0.11.xGitHub Actions、Docker 和索引行为依据 2026-07-21 的 uv 官方文档复核。CI 中应定期升级并重新验证固定版本。7.1 生产交付的四条底线本地执行成功不代表能够稳定交付。CI 和容器至少满足输入可追踪代码提交、Python、uv、锁文件和基础镜像都有明确版本构建可重复CI 不静默改锁文件容器不复制本机.venv权限最小化测试 job 没有发布权限长期 token 不进入镜像和日志缓存可丢弃删除全部缓存后仍能得到正确结果。仅加速仅加速Git 提交pyproject.toml uv.lockCI 校验lint / type / test构建产物wheel / sdist / image隔离验证SBOM / 扫描 / 冒烟受保护发布OIDC / 审批uv 缓存缓存只是一条虚线。如果移除缓存后构建失败问题在声明或环境而不是“缓存配置不够好”。7.2 GitHub Actions最小可靠工作流.github/workflows/ci.ymlname:cion:pull_request:push:branches:[main]permissions:contents:readconcurrency:group:ci-${{github.workflow}}-${{github.ref}}cancel-in-progress:truejobs:test:name:Python ${{matrix.python-version}}/ ${{matrix.os}}runs-on:${{matrix.os}}strategy:fail-fast:falsematrix:os:[ubuntu-latest,windows-latest]python-version:[3.12,3.14]steps:-name:Check out sourceuses:actions/checkoutv7-name:Install uv and Pythonuses:astral-sh/setup-uv08807647e7069bb48b6ef5acd8ec9567f424441b# v8.1.0with:version:0.11.30python-version:${{matrix.python-version}}enable-cache:truecache-dependency-glob:uv.lock-name:Verify lockfile and syncrun:uv sync--locked--all-groups--all-extras-name:Check formattingrun:uv run--locked ruff format--check .-name:Lintrun:uv run--locked ruff check .-name:Type checkrun:uv run--locked mypy src-name:Testrun:uv run--locked pytest--cov--cov-reportterm-missing-name:Minimize persistent uv cacheif:always()run:uv cache prune--ci7.2.1 为什么固定 uv 和 Actionversion: 0.11.30防止 Runner 某天自动切换 uv 行为。setup-uv固定到完整提交 SHA降低 tag 被移动带来的供应链风险。上例为易读仍使用actions/checkoutv7。高安全仓库应把所有第三方 Action包括官方 Action固定到审核过的完整 SHA并由 Dependabot/Renovate 提交升级 PR。7.2.2 为什么用--lockedCI 的职责是验证仓库状态不是替开发者生成新锁文件。若pyproject.toml与uv.lock不一致--locked应立即失败uv lock gitdiff--pyproject.toml uv.lock在本地解决并提交而不是在 workflow 中执行普通uv lock后继续。7.2.3 Python 矩阵如何选择库项目至少测试requires-python的最低支持版本团队默认版本当前稳定 Python。应用项目可以只测试实际部署版本再额外增加升级预演。矩阵中的3.14是本文时点示例复制工作流时应按项目真实支持范围调整。7.3 拆分快速检查与完整矩阵在每个 OS/Python 组合重复 Ruff 和 Mypy 往往没有收益。大型项目可拆为Pull Request快速检查Ruff Mypy lock check测试矩阵Python × OS合并门禁main/tag 构建受保护发布快速 jobquality:runs-on:ubuntu-lateststeps:-uses:actions/checkoutv7-uses:astral-sh/setup-uv08807647e7069bb48b6ef5acd8ec9567f424441bwith:version:0.11.30enable-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 membersrun:uv sync--locked--all-packages--all-groups-name:Test all membersrun:uv run--all-packages pytest-name:Build all publishable membersrun:uv build--all-packages--clear--no-sources为防止共享环境掩盖未声明依赖还应为重要成员增加隔离 job-name:Test weather-core as a package targetrun:|uv sync --locked --package weather-core uv run --package weather-core pytest packages/weather-core/tests大仓库按变更范围优化时必须包含反向依赖。核心库变化不能只测试核心库自身。7.5 正确缓存 uvsetup-uv内置缓存-uses:astral-sh/setup-uv08807647e7069bb48b6ef5acd8ec9567f424441bwith:version:0.11.30enable-cache:truecache-dependency-glob:uv.lock7.5.1 缓存键包含什么通常至少包含操作系统和架构uv 缓存格式相关信息uv.lock哈希必要时包含 Python 版本或构建工具输入。不要缓存整个.venv作为跨 Runner 复用策略。虚拟环境含绝对路径、解释器引用和平台二进制缓存 uv 下载/构建产物再用锁文件快速重建环境更稳妥。7.5.2uv cache prune --ciCI 结束时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-latestenvironment:pypipermissions:contents:readid-token:writesteps:-uses:actions/checkoutv7-uses:astral-sh/setup-uv08807647e7069bb48b6ef5acd8ec9567f424441bwith: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 --fromghcr.io/astral-sh/uv:0.11.30 /uv /uvx /bin/生产环境优先固定镜像 digestCOPY --fromghcr.io/astral-sh/uvsha256:审核过的摘要 /uv /uvx /bin/不要复制文档中的示例摘要后长期不更新应从组织信任的镜像仓库获取、验证并由自动化升级。7.8 单包项目的生产 Dockerfile# syntaxdocker/dockerfile:1.7 ARG PYTHON_IMAGEpython:3.12-slim-trixie ARG UV_IMAGEghcr.io/astral-sh/uv:0.11.30 FROM ${UV_IMAGE} AS uv-bin FROM ${PYTHON_IMAGE} AS builder COPY --fromuv-bin /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE1 \ UV_LINK_MODEcopy WORKDIR /app # 依赖层源码变化不会使其失效 COPY pyproject.toml uv.lock README.md ./ RUN --mounttypecache,target/root/.cache/uv \ uv sync --locked --no-dev --no-install-project # 项目层 COPY src ./src RUN --mounttypecache,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 --frombuilder /app/.venv /app/.venv ENV PATH/app/.venv/bin:$PATH \ PYTHONDONTWRITEBYTECODE1 \ PYTHONUNBUFFERED1 USER app ENTRYPOINT [weather] CMD [Shanghai]7.8.1 为什么分两次同步第一次只复制依赖声明和锁文件uv sync --locked --no-dev --no-install-project它安装传递依赖不安装频繁变化的当前项目。源码复制后第二次同步安装项目。业务代码变化不会使整个依赖层失效。7.8.2 为什么使用UV_LINK_MODEcopyBuildKit 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.tomluv 无法验证锁文件是否与所有成员一致。因此官方建议# syntaxdocker/dockerfile:1.7 FROM python:3.12-slim-trixie AS builder COPY --fromghcr.io/astral-sh/uv:0.11.30 /uv /uvx /bin/ ENV UV_LINK_MODEcopy \ UV_COMPILE_BYTECODE1 WORKDIR /app # 此阶段没有成员 pyproject.toml跳过新鲜度检查和成员安装 RUN --mounttypecache,target/root/.cache/uv \ --mounttypebind,sourceuv.lock,targetuv.lock \ --mounttypebind,sourcepyproject.toml,targetpyproject.toml \ uv sync --frozen --no-dev --no-install-workspace COPY . /app # 看到完整 workspace 后必须严格校验 RUN --mounttypecache,target/root/.cache/uv \ uv sync --locked --no-dev --no-editable --package weather-cli这里早期使用--frozen不是因为它更严格而是因为缺少成员元数据无法执行完整新鲜度检查。复制完整仓库后最终--locked必须成功。7.11 容器安全与可重复性7.11.1 基础镜像固定 Python 小版本或 digest定期重建以获取系统安全更新slim 镜像可能缺编译器和共享库构建扩展时使用 builderAlpine 使用 musl不应假设 manylinux wheel 可直接复用选择镜像前检查目标依赖是否提供对应 wheel。7.11.2 非 root 运行最终镜像创建专用用户并USER app。若程序需要写目录应明确创建并授权而不是把整个/app设为 777。7.11.3 字节码UV_COMPILE_BYTECODE1可减少首次启动编译成本但会增加构建时间和镜像体积。短生命周期 CLI 未必收益明显Web 服务和无服务器冷启动场景应测量后决定。7.11.4 构建秘密私有索引凭据不要用ARG或ENV烘焙进镜像层。使用 BuildKit secret mountRUN --mounttypesecret,iduv_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.1explicit true只有通过[tool.uv.sources]显式绑定的包才能从该索引安装。这样不会因为添加私有索引就让所有公共依赖都从私有源搜索。7.12.2 默认first-indexuv 默认对一个包停在第一个包含它的索引并只在该索引的候选版本中解析。这与 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 secretuv 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 SBOMuv export--format cyclonedx1.5--output-file sbom.jsonSBOM 应与具体提交、锁文件和构建产物关联。它列出组件不自动判断漏洞是否可利用。7.14.3 发布证明PyPI Trusted Publishing、容器 provenance 和签名各自解决不同问题身份、构建来源和产物完整性。生产流程应保存Git commit/tagCI run IDuv.lock哈希wheel/sdist/image digestSBOM 和扫描结果发布环境审批记录。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_MODEcopyWorkspace 早期依赖层报锁文件过期早期层缺少成员元数据使用--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 生产验收清单CIuv、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 迁移的分阶段方案并建立覆盖解释器、解析、构建、网络和缓存的系统排障方法。官方参考Using uv in GitHub ActionsUsing uv in DockerPackage indexesCachingPyPI Trusted Publishers