跳转至

进阶 4 · 打包发布与部署

代码写完、依赖锁定、配置就绪——最后一公里:把项目打包成可分发的产物、发布到 PyPI、用 Docker 容器化部署、用 CI/CD 自动化。本章用两个贯穿项目做真实示范:logstats(打包成 CLI 发 PyPI)、bookmarks-api(Docker 部署)。


4.1 包是什么:wheel 与 sdist

Python 的发行包有两种格式(对照 Java 的 jar):

格式 内容 何时用
wheel (.whl) 预编译的"即装即用"包(zip 格式) 首选,安装快
sdist (.tar.gz) 源码发行版(需构建) wheel 不够时的后备

纯 Python 项目生成的是 py3-none-any.whl(任何平台通用);含 C 扩展的会按平台生成(如 cp313-win_amd64.whl)。


4.2 构建后端与前端

打包分两层(这是新手最易混的):

  • 构建后端pyproject.toml[build-system]):实际干活的工具。本教程主推 hatchling
  • 构建前端(你敲的命令):调用后端生成产物。uv 路线 uv build,pip 路线 python -m build
# pyproject.toml —— 声明用 hatchling 构建
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

4.3 完整的可发布 pyproject.toml

logstats 就是一个可发布的库/CLI。关键配置:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "logstats"                    # PyPI 上的包名
version = "0.1.0"                    # 版本号(见 4.7)
description = "Analyze access logs"
requires-python = ">=3.13"
dependencies = []                    # 纯标准库,无运行依赖

[project.scripts]
logstats = "logstats.cli:main"       # 安装后提供 `logstats` 命令!

[tool.hatch.build.targets.wheel]
packages = ["src/logstats"]          # 告诉 hatchling 源码在哪(src 布局)

[project.scripts] 是关键——它让 pip install logstats 后,命令行直接能用 logstats(对照 Maven 的可执行入口、npm 的 bin)。对照 Java:相当于装一个带 main 的 jar 并注册成命令。


4.4 构建包(实证)

cd project
uv build                  # uv 路线:生成 dist/
# 或 pip 路线:pip install build && python -m build

实际运行结果(本仓库验证过):

dist/
├── logstats-0.1.0-py3-none-any.whl    # wheel(5 KB)
└── logstats-0.1.0.tar.gz              # sdist(5 KB)

uv build 用 hatchling 后端,干净生成两个产物——没有任何额外配置就成功,说明 4.3 的 pyproject.toml 配置正确。


4.5 本地验证安装

发布前,先在干净环境验证 wheel 能装、命令能用:

# 新建一个隔离环境测试(不污染开发环境)
uv venv /tmp/test-env && uv pip install --python /tmp/test-env dist/logstats-0.1.0-py3-none-any.whl
/tmp/test-env/bin/logstats --help          # 命令可用 → 入口点 OK(Windows 用 Scripts/)

或用 pip:

python -m venv /tmp/test-env && /tmp/test-env/bin/pip install dist/logstats-0.1.0-py3-none-any.whl
/tmp/test-env/bin/logstats --help

Pythonic 实践

发布前必做这一步——很多"本地能跑但发布后装不上"的问题(缺文件、入口点错)在这一步暴露。


4.6 发布到 PyPI

PyPI 是 Python 的中央仓库(对照 Maven Central)。先在 TestPyPI 试发(隔离的测试仓库),确认无误再发正式。

# 1. 在 pypi.org 注册账号,生成 API token
# 2. 先发 TestPyPI 试水
uv publish --publish-url https://test.pypi.org/legacy/ --token pypi-xxxx
# 3. 正式发布
uv publish --token pypi-xxxx
# 或 pip 路线:twine upload dist/*

发布后,全世界都能 pip install logstats / uv add logstats

版本不可覆盖:PyPI 上同一版本号只能发一次。要再发必须升版本号(4.7)。


4.7 版本号

推荐语义化版本 MAJOR.MINOR.PATCH(对照 Maven 的版本):

  • 0.1.00.1.1:修 bug(PATCH)
  • 0.1.00.2.0:加功能、向后兼容(MINOR)
  • 0.1.01.0.0:破坏性变更(MAJOR)

0.x.y 表示"还不稳定、API 可能变"(很多库长期在 0.x)。版本号写在 pyproject.tomlversion,也可动态从 __init__.py 读(hatchling 支持 dynamic = ["version"]),避免两处维护。


4.8 Docker 部署(多阶段 + uv cache)

bookmarks-api 是 FastAPI 服务,适合容器化。多阶段构建让最终镜像小、层缓存好。

# bookmarks-api/Dockerfile
# 阶段 1:构建依赖(变化少,缓存命中率高)
FROM python:3.13-slim AS builder
RUN pip install uv
WORKDIR /app
COPY pyproject.toml ./
# 先装依赖、不装项目本身(这层缓存命中率高;--no-dev 排除测试依赖)
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --no-dev --no-install-project
COPY src/ ./src/
# 再装项目本身
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --no-dev

# 阶段 2:运行时(小镜像,不含构建工具)
FROM python:3.13-slim
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY src/ ./src/
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["uvicorn", "bookmarks_api.main:app", "--host", "0.0.0.0", "--port", "8000"]

四个关键点:

  1. 多阶段:builder 装依赖,runtime 只复制 .venv + 源码——最终镜像不含 uv/编译器,更小更安全。
  2. COPY pyproject.toml 在源码前:依赖变化少,先复制能让"装依赖"层命中缓存(源码天天改,放最后)。两次 uv sync:先 --no-install-project 只装依赖(缓存层),再装项目本身。
  3. --mount=type=cache,target=/root/.cache/uv:uv 的下载缓存跨构建复用,加速重建。
  4. --no-dev:排除 pytest/ruff 等开发依赖,镜像精简。若你提交了 uv.lock(生产推荐),把 COPY pyproject.toml ./ 改为 COPY pyproject.toml uv.lock ./,两条 uv sync--frozen 严格按锁文件精确复现版本。
docker build -t bookmarks-api:latest .
docker run -p 8000:8000 -e DATABASE_URL=sqlite+aiosqlite:///./data.db bookmarks-api:latest

配套 .dockerignore(避免把 .venvtests.git 拷进镜像):

.venv/
.git/
tests/
__pycache__/
*.pyc
.env

对照 Spring Boot

Spring Boot 常用 layered jar + 多阶段 Docker;FastAPI 这边是 .venv + 多阶段。思路一致:分层 + 小镜像 + 缓存。


4.9 生产 ASGI:多 worker

uvicorn 是单进程,多核机器要跑多 worker。生产用 Gunicorn 管理 uvicorn worker

# 单机多核
gunicorn bookmarks_api.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
# 或 uvicorn 自己的多 worker(更简单)
uvicorn bookmarks_api.main:app --workers 4

容器里通常 --workers 2(按 CPU 配额),前面再放 Nginx/Caddy 做 TLS、负载均衡。


4.10 CI/CD:通用流水线概念

无论 GitHub Actions / GitLab CI / Jenkins,Python 项目的 CI/CD 流水线结构相似:

push/PR → 安装(uv sync) → lint(ruff) → 类型检查(mypy) → 测试(pytest) → 构建(uv build) → [发版时] 发布(uv publish)

以 GitHub Actions 为例(其他平台同理,换语法):

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v6        # uv 官方 action
      - run: uv sync                        # 装依赖(读 uv.lock)
      - run: uv run ruff check .
      - run: uv run pytest
  publish:
    if: startsWith(github.ref, 'refs/tags/v')   # 打 tag 才发布
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v6
      - run: uv build
      - run: uv publish                  # token 用 PyPI 的 trusted publishing 更安全
        env:
          UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}

可信发布(Trusted Publishing):现代推荐做法——不在 CI 里存 PyPI token,而是配置 PyPI 信任特定 GitHub 仓库/工作流直接发布(OIDC),更安全。

核心概念(平台无关)

  1. 依赖缓存:缓存 .venv/uv cache,加速流水线。
  2. 矩阵测试:跨 Python 版本/操作系统测(matrix: python-version: [3.12, 3.13])。
  3. 发版分离:测试每次 PR 跑,发布只在打 tag 时跑。
  4. 密钥用平台 secrets:不硬编码 token。

全链路回顾

环境(第1章)→ 依赖(第2章)→ 配置(第3章)→ 打包构建(4.4)→ 本地验证(4.5)
          → 发布 PyPI(4.6)→ Docker 镜像(4.8)→ 生产部署(4.9)→ CI/CD 自动化(4.10)

走完这条链,你已能把一个 Python 项目从开发带到生产——且全程对照 Java/Maven/Spring 的工程化心智模型。


本章练习

练习 4.1

logstats 运行 uv build,说明生成的两个文件分别是什么、为什么 wheel 优先。

参考答案

uv build 生成 logstats-0.1.0-py3-none-any.whl(wheel,即装即用的 zip)和 logstats-0.1.0.tar.gz(sdist,源码)。wheel 优先因为安装快(无需构建步骤)、确定性高;sdist 作为后备(wheel 不支持的平台/需要本地构建时)。

练习 4.2

解释为什么 Dockerfile 里依赖相关文件pyproject.toml,生产环境含 uv.lock)的 COPY 要放在 COPY src/ 之前。

参考答案

Docker 按指令顺序分层缓存。依赖文件(pyproject/lock)变化少,放前面能让"装依赖"这层在源码改动时仍命中缓存,避免每次改代码都重装依赖(最耗时)。源码天天改,放最后——改动只使最后一层及之后失效。

练习 4.3

设计一条 CI 流水线:PR 时跑测试,打 v* tag 时发布到 PyPI。说明为什么发布要和测试分开。

参考答案

测试 job 每次 PR/push 跑(on: [pull_request]),保证代码质量;发布 job 用 if: startsWith(github.ref, 'refs/tags/v') 只在打 tag 时跑。分开是因为:发布不可逆(PyPI 版本不可覆盖)、发布需要 secrets(缩小暴露面)、测试要频繁跑而发布要克制。

练习 4.4

生产环境为什么用 --no-dev 构建、为什么密钥不打进镜像?

参考答案

--no-dev 排除 pytest/ruff 等开发依赖,镜像更小、攻击面更小(生产不需要测试工具)。密钥打进镜像 = 镜像含密钥,一旦镜像分发/泄露,密钥即泄露(且不可通过删镜像回收,因为可能已被拉取)。正确:镜像不含密钥,运行时用 -e/k8s Secret 注入环境变量。


上一章:进阶 3 · 配置与 .env← 回首页核心教程路线图