Python 部署与分发:Docker、PyPI 发布与可复现环境

Python 项目从开发到生产的完整部署路径:Docker 多阶段构建与镜像优化、uvicorn/gunicorn 服务器配置、pyinstaller/uv 打包独立可执行文件、PyPI 包发布流程、Nix 可复现环境。附带 Dockerfile 模板和 GitHub Actions 发布流水线。

「在我的机器上可以跑」是部署的原罪。本文将 Python 应用从开发环境「封印」到生产环境,确保任何机器上行为一致。


1. Docker 多阶段构建

1.1 基础 Dockerfile

# ========== Stage 1: Builder ==========
FROM python:3.12-slim as builder

WORKDIR /app

# 安装构建依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# 安装 Python 依赖到独立目录
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# ========== Stage 2: Production ==========
FROM python:3.12-slim

WORKDIR /app

# 创建非 root 用户
RUN useradd -m -u 1000 appuser

# 只复制必要的依赖
COPY --from=builder /root/.local /home/appuser/.local
COPY ./app ./app

# 设置环境
ENV PATH=/home/appuser/.local/bin:$PATH \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

USER appuser

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

1.2 基于 uv 的优化版

# 最小化镜像(基于 uv 官方镜像)
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

WORKDIR /app

# 启用编译缓存
ENV UV_COMPILE_BYTECODE=1

# 先复制 lock 文件(利用 Docker 层缓存)
COPY uv.lock pyproject.toml ./
RUN uv sync --frozen --no-install-project --no-dev

# 再复制代码
COPY ./app ./app
RUN uv sync --frozen --no-dev

# 运行
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

镜像大小对比:

方案大小说明
单阶段(全量)1.2GB包含 gcc 等构建工具
多阶段 pip180MB分离构建与运行
多阶段 uv85MB最小化依赖
distroless65MB无 shell,最安全

1.3 健康检查与优雅关闭

# Dockerfile 中添加
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# app/main.py — 优雅关闭处理
import asyncio
import signal
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动
    await connect_db()
    yield
    # 关闭
    await close_db()

app = FastAPI(lifespan=lifespan)

@app.get("/health")
async def health():
    return {"status": "healthy"}

2. 服务器配置:uvicorn vs gunicorn

2.1 选型

服务器适用场景特点
uvicorn开发、单进程服务原生 ASGI,最简单
gunicorn + uvicorn workers生产多核进程管理 + 异步处理
hypercornHTTP/2、WebSocket协议支持最全
daphneDjango ChannelsDjango 生态

2.2 生产配置

# gunicorn + uvicorn(推荐)
gunicorn app.main:app \
    --workers 4 \
    --worker-class uvicorn.workers.UvicornWorker \
    --bind 0.0.0.0:8000 \
    --access-logfile - \
    --error-logfile - \
    --log-level info \
    --timeout 120 \
    --keep-alive 5 \
    --max-requests 10000 \
    --max-requests-jitter 1000

# 或使用配置文件
cat > gunicorn.conf.py << 'EOF'
import multiprocessing

bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
timeout = 120
keepalive = 5
errorlog = "-"
accesslog = "-"
EOF

gunicorn -c gunicorn.conf.py app.main:app

2.3 systemd 服务

# /etc/systemd/system/myapp.service
[Unit]
Description=My Python App
After=network.target

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
Environment=PATH=/opt/myapp/.venv/bin
Environment=DATABASE_URL=postgresql://...
ExecStart=/opt/myapp/.venv/bin/gunicorn -c gunicorn.conf.py app.main:app
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl restart myapp

3. 打包独立可执行文件

3.1 PyInstaller

pip install pyinstaller

# 单文件打包
pyinstaller --onefile --name mytool app/cli.py

# 带图标和数据文件
pyinstaller \
    --onefile \
    --name mytool \
    --icon=assets/icon.ico \
    --add-data "config.yaml:." \
    app/cli.py

# 输出在 dist/mytool

3.2 uv 打包(实验性)

# uv 支持将项目打包为 zipapp(单文件,无需安装)
uv pip install . --target dist/
python -m zipapp dist/ -p "/usr/bin/env python3" -o myapp.pyz

# 运行
python myapp.pyz

3.3 平台分发对比

工具输出大小跨平台场景
PyInstaller单二进制10-50MB❌ 需各平台构建GUI/CLI 工具
zipapp.pyz纯 Python 脚本
Docker容器镜像服务端部署
nuitkaC 编译较小性能敏感

4. PyPI 发布

4.1 项目结构

my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│   └── my_package/
│       ├── __init__.py
│       └── core.py
└── tests/
    └── test_core.py

4.2 pyproject.toml 发布配置

[project]
name = "my-package"
version = "0.1.0"
description = "A useful Python package"
readme = "README.md"
license = { text = "MIT" }
authors = [
    { name = "Your Name", email = "you@example.com" }
]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]
keywords = ["utility", "tool"]
requires-python = ">=3.11"
dependencies = ["requests>=2.30", "pydantic>=2.0"]

[project.optional-dependencies]
dev = ["pytest", "mypy", "ruff"]

[project.urls]
Homepage = "https://github.com/you/my-package"
Repository = "https://github.com/you/my-package.git"
Issues = "https://github.com/you/my-package/issues"

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

[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]

4.3 构建与上传

# 安装构建工具
pip install build twine

# 构建(生成 dist/*.whl 和 dist/*.tar.gz)
python -m build

# 检查
python -m twine check dist/*

# 上传到 TestPyPI(测试)
python -m twine upload --repository testpypi dist/*

# 上传到正式 PyPI
python -m twine upload dist/*

4.4 GitHub Actions 自动发布

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  pypi-publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # OIDC 认证,无需 API token
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Install uv
        uses: astral-sh/setup-uv@v2
      
      - name: Sync
        run: uv sync
      
      - name: Build
        run: uv run python -m build
      
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1

设置 PyPI Trusted Publisher:

  1. PyPI 项目设置 → Publishing
  2. Add a new pending publisher
  3. 填写 GitHub 仓库和 workflow 文件名
  4. 无需存储 API token!

5. Nix:终极可复现环境

5.1 基础 flake.nix

{
  description = "My Python App";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        python = pkgs.python312;
      in
      {
        devShells.default = pkgs.mkShell {
          packages = [
            python
            python.pkgs.uv
            python.pkgs.ruff
            python.pkgs.mypy
          ];
        };

        packages.default = python.pkgs.buildPythonApplication {
          pname = "myapp";
          version = "0.1.0";
          src = ./.;
          propagatedBuildInputs = [
            python.pkgs.fastapi
            python.pkgs.uvicorn
          ];
        };
      });
}

使用:

# 进入开发环境(所有依赖精确锁定)
nix develop

# 构建
nix build

# 运行
./result/bin/myapp

6. 部署清单

6.1 生产检查表

  • 使用非 root 用户运行
  • 启用 HEALTHCHECK
  • 配置日志轮转(logrotate)
  • 设置资源限制(ulimit, cgroups)
  • 配置反向代理(Nginx/Traefik)
  • 启用 HTTPS(Let’s Encrypt)
  • 设置环境变量(不硬编码密钥)
  • 配置监控(Prometheus metrics)
  • 配置告警(CPU/内存/磁盘)

6.2 docker-compose 生产模板

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=myapp

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app

volumes:
  postgres_data:
  redis_data:

延伸阅读

下一篇 →

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章