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

核心目标:把本地 uv 工作流可靠迁移到 GitHub Actions 和生产容器,正确使用锁文件、缓存、私有索引和短期凭据,建立可审计的交付链路。

前置知识:已掌握 uv 项目、锁文件、构建发布和 workspace。

文档基线:uv 0.11.x;GitHub Actions、Docker 和索引行为依据 2026-07-21 的 uv 官方文档复核。CI 中应定期升级并重新验证固定版本。


7.1 生产交付的四条底线

本地执行成功不代表能够稳定交付。CI 和容器至少满足:

  1. 输入可追踪:代码提交、Python、uv、锁文件和基础镜像都有明确版本;
  2. 构建可重复 :CI 不静默改锁文件,容器不复制本机 .venv
  3. 权限最小化:测试 job 没有发布权限,长期 token 不进入镜像和日志;
  4. 缓存可丢弃:删除全部缓存后仍能得到正确结果。

#mermaid-svg-JebYDxhyCXl6pNhP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JebYDxhyCXl6pNhP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JebYDxhyCXl6pNhP .error-icon{fill:#552222;}#mermaid-svg-JebYDxhyCXl6pNhP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JebYDxhyCXl6pNhP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JebYDxhyCXl6pNhP .marker.cross{stroke:#333333;}#mermaid-svg-JebYDxhyCXl6pNhP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JebYDxhyCXl6pNhP p{margin:0;}#mermaid-svg-JebYDxhyCXl6pNhP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster-label text{fill:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster-label span{color:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster-label span p{background-color:transparent;}#mermaid-svg-JebYDxhyCXl6pNhP .label text,#mermaid-svg-JebYDxhyCXl6pNhP span{fill:#333;color:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .node rect,#mermaid-svg-JebYDxhyCXl6pNhP .node circle,#mermaid-svg-JebYDxhyCXl6pNhP .node ellipse,#mermaid-svg-JebYDxhyCXl6pNhP .node polygon,#mermaid-svg-JebYDxhyCXl6pNhP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JebYDxhyCXl6pNhP .rough-node .label text,#mermaid-svg-JebYDxhyCXl6pNhP .node .label text,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape .label,#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape .label{text-anchor:middle;}#mermaid-svg-JebYDxhyCXl6pNhP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JebYDxhyCXl6pNhP .rough-node .label,#mermaid-svg-JebYDxhyCXl6pNhP .node .label,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape .label,#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape .label{text-align:center;}#mermaid-svg-JebYDxhyCXl6pNhP .node.clickable{cursor:pointer;}#mermaid-svg-JebYDxhyCXl6pNhP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JebYDxhyCXl6pNhP .arrowheadPath{fill:#333333;}#mermaid-svg-JebYDxhyCXl6pNhP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JebYDxhyCXl6pNhP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JebYDxhyCXl6pNhP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JebYDxhyCXl6pNhP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JebYDxhyCXl6pNhP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JebYDxhyCXl6pNhP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JebYDxhyCXl6pNhP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster text{fill:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster span{color:#333;}#mermaid-svg-JebYDxhyCXl6pNhP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JebYDxhyCXl6pNhP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JebYDxhyCXl6pNhP rect.text{fill:none;stroke-width:0;}#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape p,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape .label rect,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JebYDxhyCXl6pNhP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JebYDxhyCXl6pNhP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JebYDxhyCXl6pNhP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 仅加速
仅加速
Git 提交

pyproject.toml + uv.lock
CI 校验

lint / type / test
构建产物

wheel / sdist / image
隔离验证

SBOM / 扫描 / 冒烟
受保护发布

OIDC / 审批
uv 缓存

缓存只是一条虚线。如果移除缓存后构建失败,问题在声明或环境,而不是"缓存配置不够好"。


7.2 GitHub Actions:最小可靠工作流

.github/workflows/ci.yml

yaml 复制代码
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.tomluv.lock 不一致,--locked 应立即失败:

powershell 复制代码
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 往往没有收益。大型项目可拆为:
#mermaid-svg-Za1mF98VXmes4sff{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Za1mF98VXmes4sff .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Za1mF98VXmes4sff .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Za1mF98VXmes4sff .error-icon{fill:#552222;}#mermaid-svg-Za1mF98VXmes4sff .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Za1mF98VXmes4sff .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Za1mF98VXmes4sff .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Za1mF98VXmes4sff .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Za1mF98VXmes4sff .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Za1mF98VXmes4sff .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Za1mF98VXmes4sff .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Za1mF98VXmes4sff .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Za1mF98VXmes4sff .marker.cross{stroke:#333333;}#mermaid-svg-Za1mF98VXmes4sff svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Za1mF98VXmes4sff p{margin:0;}#mermaid-svg-Za1mF98VXmes4sff .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Za1mF98VXmes4sff .cluster-label text{fill:#333;}#mermaid-svg-Za1mF98VXmes4sff .cluster-label span{color:#333;}#mermaid-svg-Za1mF98VXmes4sff .cluster-label span p{background-color:transparent;}#mermaid-svg-Za1mF98VXmes4sff .label text,#mermaid-svg-Za1mF98VXmes4sff span{fill:#333;color:#333;}#mermaid-svg-Za1mF98VXmes4sff .node rect,#mermaid-svg-Za1mF98VXmes4sff .node circle,#mermaid-svg-Za1mF98VXmes4sff .node ellipse,#mermaid-svg-Za1mF98VXmes4sff .node polygon,#mermaid-svg-Za1mF98VXmes4sff .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Za1mF98VXmes4sff .rough-node .label text,#mermaid-svg-Za1mF98VXmes4sff .node .label text,#mermaid-svg-Za1mF98VXmes4sff .image-shape .label,#mermaid-svg-Za1mF98VXmes4sff .icon-shape .label{text-anchor:middle;}#mermaid-svg-Za1mF98VXmes4sff .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Za1mF98VXmes4sff .rough-node .label,#mermaid-svg-Za1mF98VXmes4sff .node .label,#mermaid-svg-Za1mF98VXmes4sff .image-shape .label,#mermaid-svg-Za1mF98VXmes4sff .icon-shape .label{text-align:center;}#mermaid-svg-Za1mF98VXmes4sff .node.clickable{cursor:pointer;}#mermaid-svg-Za1mF98VXmes4sff .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Za1mF98VXmes4sff .arrowheadPath{fill:#333333;}#mermaid-svg-Za1mF98VXmes4sff .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Za1mF98VXmes4sff .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Za1mF98VXmes4sff .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Za1mF98VXmes4sff .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Za1mF98VXmes4sff .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Za1mF98VXmes4sff .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Za1mF98VXmes4sff .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Za1mF98VXmes4sff .cluster text{fill:#333;}#mermaid-svg-Za1mF98VXmes4sff .cluster span{color:#333;}#mermaid-svg-Za1mF98VXmes4sff div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Za1mF98VXmes4sff .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Za1mF98VXmes4sff rect.text{fill:none;stroke-width:0;}#mermaid-svg-Za1mF98VXmes4sff .icon-shape,#mermaid-svg-Za1mF98VXmes4sff .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Za1mF98VXmes4sff .icon-shape p,#mermaid-svg-Za1mF98VXmes4sff .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Za1mF98VXmes4sff .icon-shape .label rect,#mermaid-svg-Za1mF98VXmes4sff .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Za1mF98VXmes4sff .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Za1mF98VXmes4sff .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Za1mF98VXmes4sff :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Pull Request
快速检查

Ruff + Mypy + lock check
测试矩阵

Python × OS
合并门禁
main/tag 构建
受保护发布

快速 job:

yaml 复制代码
  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

最可靠的基线是全量安装和测试:

yaml 复制代码
- 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:

yaml 复制代码
- 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 内置缓存:

yaml 复制代码
- 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 结束时:

powershell 复制代码
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 应独立,并依赖构建验证:

yaml 复制代码
  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 镜像。常见做法:

dockerfile 复制代码
FROM python:3.12-slim-trixie

COPY --from=ghcr.io/astral-sh/uv:0.11.30 /uv /uvx /bin/

生产环境优先固定镜像 digest:

dockerfile 复制代码
COPY --from=ghcr.io/astral-sh/uv@sha256:<审核过的摘要> /uv /uvx /bin/

不要复制文档中的示例摘要后长期不更新;应从组织信任的镜像仓库获取、验证并由自动化升级。


7.8 单包项目的生产 Dockerfile

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 为什么分两次同步

第一次只复制依赖声明和锁文件:

dockerfile 复制代码
uv sync --locked --no-dev --no-install-project

它安装传递依赖,不安装频繁变化的当前项目。源码复制后第二次同步安装项目。业务代码变化不会使整个依赖层失效。

BuildKit cache mount 与目标 .venv 可能位于不同文件系统,硬链接不可用。设置 copy 可避免链接警告,并确保最终镜像层不依赖已卸载的 cache mount。

7.8.3 为什么最终镜像不包含 uv

运行时只需要 .venv 中的 Python 和入口脚本。将 uv 留在 builder 可缩小攻击面。若生产运维确实需要 uv run,可以复制 uv,但要明确理由。


7.9 .dockerignore 不可省略

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 无法验证锁文件是否与所有成员一致。因此官方建议:

dockerfile 复制代码
# 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 构建秘密

私有索引凭据不要用 ARGENV 烘焙进镜像层。使用 BuildKit secret mount:

dockerfile 复制代码
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 私有索引配置

toml 复制代码
[[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 风险。

不要为了"版本更新"随意启用:

powershell 复制代码
uv sync --index-strategy unsafe-best-match

它会合并多个索引候选,更接近 pip,但显著扩大同名恶意包风险。优先修复索引顺序、同步代理或显式包绑定。

7.12.3 凭据环境变量

索引名 internal 对应:

powershell 复制代码
$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:

toml 复制代码
[[tool.uv.index]]
name = "offline"
url = "./wheelhouse"
format = "flat"
explicit = true

离线交付必须覆盖目标平台、Python ABI 和所有传递依赖。只在联网开发机下载一次并不等于完成离线验证。


7.14 供应链控制

7.14.1 时间冷却

toml 复制代码
[tool.uv]
exclude-newer = "7 days"

冷却期能避免立即采用刚上传的发行物,为社区和安全系统留出观察时间。它会降低更新速度,安全补丁需要例外流程。

7.14.2 SBOM

powershell 复制代码
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 失败

按顺序检查:

  1. CI Python 是否在 requires-python 范围内;
  2. 是否提交了最新 uv.lock
  3. 本地是否依赖未声明的全局包;
  4. 目标平台是否有兼容 wheel 或编译工具;
  5. 私有索引和凭据是否只在本机配置;
  6. 删除缓存后是否仍失败。

Docker 每次都重新安装依赖

确认 COPY . /app 没有发生在依赖层之前。先复制 pyproject.tomluv.lock 和构建元数据,再执行 --no-install-project

容器出现跨文件系统链接警告

在 BuildKit cache mount 场景设置:

dockerfile 复制代码
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 迁移的分阶段方案,并建立覆盖解释器、解析、构建、网络和缓存的系统排障方法。

官方参考

相关推荐
邪神与厨二病1 小时前
牛客周赛 Round 153
python·算法
yaoxin5211231 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python
groundhappy2 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
培之2 小时前
RTX 5090 安装 pytorch3d
人工智能·pytorch·python
Metaphor6923 小时前
使用 Python 在 PowerPoint 中创建折线图和条形图
python·信息可视化·powerpoint·图表
m沐沐4 小时前
【深度学习】卷积神经网络 数据增强、保存最优模型实现,详细解读
人工智能·python·深度学习·机器学习·cnn·数据增强
天青色等烟雨..4 小时前
全流程ArcGISPro空间分析、三维建模、可视化及Python融合应用技术
开发语言·python
极光代码工作室4 小时前
基于Spark的日志监控与分析平台
大数据·hadoop·python·spark·数据可视化
diving deep4 小时前
CI/CD
ci/cd