Linux + Docker + FastAPI 工程化实践指南

一套稳健的 FastAPI 工程,不是把 API、Redis 和 PostgreSQL 塞进同一个 Compose 文件就算完事。更合理的做法是把应用设计成无状态服务,数据库迁移、配置管理、健康检查和测试各自承担清晰职责;开发环境追求反馈速度,生产环境则强调镜像可复现、最小权限和可观测性。

下面给出一套可以直接落地的项目骨架。

一、推荐架构

开发环境可以使用 Docker Compose 编排所有依赖,代码通过挂载目录实现热更新。生产环境仍然使用相同镜像,但不再挂载源代码,也不启用 --reload

flowchart LR C[客户端] --> N[Nginx / Traefik / LB] N --> A[FastAPI 容器] A --> P[(PostgreSQL)] A --> R[(Redis)] M[Alembic 迁移任务] --> P W[后台任务 Worker] --> P W --> R

各组件的职责建议如下。

组件 主要职责 工程建议
FastAPI HTTP 接口、业务编排 保持无状态,不在本地磁盘保存业务数据
PostgreSQL 持久化业务数据 使用 SQLAlchemy 2.x、Alembic
Redis 缓存、限流、分布式锁 不把 Redis 当作唯一可靠数据源
Worker 执行耗时任务 与 API 使用同一代码库、不同启动命令
Alembic 管理数据库结构 作为独立的一次性任务运行
反向代理 TLS、路由、限流 云环境也可以交给托管负载均衡器

FastAPI 官方现在更推荐从标准 Python 镜像自行构建,而不是依赖过去的 tiangolo/uvicorn-gunicorn-fastapi 基础镜像。容器环境中通常让每个容器运行一个应用进程,再由编排平台完成横向扩容;单机 Docker 部署则可以根据 CPU 和数据库连接预算设置多个 worker。


二、项目目录设计

目录不宜过早拆得过细,但数据库、缓存、配置和业务领域应当分开。

text 复制代码
fastapi-project/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── deps.py
│   │   └── v1/
│   │       ├── router.py
│   │       └── endpoints/
│   │           └── users.py
│   ├── core/
│   │   ├── config.py
│   │   ├── logging.py
│   │   └── security.py
│   ├── db/
│   │   ├── base.py
│   │   ├── models.py
│   │   └── session.py
│   ├── repositories/
│   │   └── user.py
│   ├── schemas/
│   │   └── user.py
│   └── services/
│       └── user.py
├── migrations/
├── tests/
│   ├── conftest.py
│   ├── integration/
│   └── unit/
├── alembic.ini
├── compose.yaml
├── compose.dev.yaml
├── Dockerfile
├── pyproject.toml
├── uv.lock
├── .dockerignore
├── .env.example
└── Makefile

这里有一条很实用的边界:

  • endpoints 负责 HTTP 输入输出。
  • schemas 保存 Pydantic 请求和响应模型。
  • services 放业务规则。
  • repositories 封装数据库访问。
  • db/models.py 保存 SQLAlchemy ORM 模型。
  • core 放跨业务的基础设施配置。

不要让路由函数直接堆几十行 SQL、缓存和权限判断。项目刚开始时似乎省事,三个月后它就会长成一团很有生命力的藤蔓。


三、依赖与配置管理

推荐使用 uv 管理 Python 依赖和锁文件,也可以换成 Poetry。核心原则是提交锁文件,并在构建时严格按锁文件安装

pyproject.toml

toml 复制代码
[project]
name = "fastapi-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "fastapi",
    "uvicorn[standard]",
    "sqlalchemy[asyncio]",
    "asyncpg",
    "alembic",
    "redis[hiredis]",
    "pydantic-settings",
    "structlog",
]

[dependency-groups]
dev = [
    "httpx",
    "pytest",
    "pytest-asyncio",
    "ruff",
    "mypy",
    "pre-commit",
]

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

正式项目最好锁定依赖版本,不要让生产镜像每次构建都随手拿到不同版本。

app/core/config.py

python 复制代码
from functools import lru_cache

from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "fastapi-project"
    environment: str = "development"
    debug: bool = False

    database_url: str
    redis_url: str

    database_pool_size: int = 10
    database_max_overflow: int = 10

    secret_key: SecretStr

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
        extra="ignore",
    )


@lru_cache
def get_settings() -> Settings:
    return Settings()


settings = get_settings()

.env.example 只保留示例,不放真实密码。

dotenv 复制代码
ENVIRONMENT=development
DEBUG=true

DATABASE_URL=postgresql+asyncpg://app:app@postgres:5432/app
REDIS_URL=redis://redis:6379/0

DATABASE_POOL_SIZE=10
DATABASE_MAX_OVERFLOW=10

SECRET_KEY=replace-me

开发环境使用 .env 没问题,生产环境则应使用 Docker Secrets、Kubernetes Secrets、Vault 或云厂商的密钥服务。秘密写进镜像或 Git 历史后,即使后来删除文件,也不能算真正删除。Docker 官方同样建议通过 secrets 传递密码、证书和令牌等敏感数据。


四、PostgreSQL 集成

FastAPI 是异步框架时,数据库层可以采用 SQLAlchemy 2.x 的异步接口和 asyncpg。每个请求创建一个短生命周期的 AsyncSession,请求结束后归还连接。

app/db/session.py

python 复制代码
from collections.abc import AsyncIterator

from sqlalchemy.ext.asyncio import (
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)

from app.core.config import settings


engine = create_async_engine(
    settings.database_url,
    pool_size=settings.database_pool_size,
    max_overflow=settings.database_max_overflow,
    pool_pre_ping=True,
    pool_recycle=1800,
    echo=False,
)

AsyncSessionFactory = async_sessionmaker(
    bind=engine,
    class_=AsyncSession,
    expire_on_commit=False,
    autoflush=False,
)


async def get_db_session() -> AsyncIterator[AsyncSession]:
    async with AsyncSessionFactory() as session:
        try:
            yield session
        except Exception:
            await session.rollback()
            raise

业务层可以显式决定事务边界。

python 复制代码
from sqlalchemy.ext.asyncio import AsyncSession

from app.db.models import User


async def create_user(session: AsyncSession, email: str) -> User:
    user = User(email=email)
    session.add(user)

    await session.commit()
    await session.refresh(user)

    return user

连接池配置的关键点

连接池不是越大越好。需要同时考虑:

  • API 容器数量
  • 每个容器的 worker 数量
  • 每个 worker 的连接池上限
  • Worker、定时任务和迁移工具占用的连接
  • PostgreSQL 的 max_connections

例如,4 个容器、每个容器 2 个进程、每个进程最多使用 20 个连接,理论峰值已经达到 160 个,还没算后台任务和运维连接。

流量较大时,可以在 PostgreSQL 前使用 PgBouncer。若采用事务级池化,需要检查预编译语句、会话变量等功能是否与池化模式兼容。

SQLAlchemy 官方文档强调,SessionAsyncSession 是有状态的事务对象,不应被多个并发任务共享;连接在事务结束后会被释放回 Engine 管理的连接池。


五、Redis 集成

现代 redis-py 已经内置 asyncio 支持,无需继续使用已经并入主项目的旧版 aioredis 包。

Redis 客户端适合在应用启动时创建,在应用关闭时统一释放,而不是每个请求都重新建连接。

app/main.py

python 复制代码
from contextlib import asynccontextmanager

import redis.asyncio as redis
from fastapi import FastAPI, Request
from sqlalchemy import text

from app.core.config import settings
from app.db.session import AsyncSessionFactory, engine


@asynccontextmanager
async def lifespan(app: FastAPI):
    redis_client = redis.from_url(
        settings.redis_url,
        encoding="utf-8",
        decode_responses=True,
        health_check_interval=30,
        socket_connect_timeout=3,
        socket_timeout=3,
        retry_on_timeout=True,
    )

    app.state.redis = redis_client

    yield

    await redis_client.aclose()
    await engine.dispose()


app = FastAPI(
    title=settings.app_name,
    lifespan=lifespan,
)


@app.get("/health/live", include_in_schema=False)
async def liveness():
    return {"status": "ok"}


@app.get("/health/ready", include_in_schema=False)
async def readiness(request: Request):
    async with AsyncSessionFactory() as session:
        await session.execute(text("SELECT 1"))

    await request.app.state.redis.ping()

    return {"status": "ready"}

Redis 依赖注入

python 复制代码
from typing import Annotated

from fastapi import Depends, Request
from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import AsyncSession

from app.db.session import get_db_session


def get_redis(request: Request) -> Redis:
    return request.app.state.redis


DBSession = Annotated[AsyncSession, Depends(get_db_session)]
RedisClient = Annotated[Redis, Depends(get_redis)]

缓存示例

python 复制代码
import json

from redis.asyncio import Redis


async def get_cached_user(redis_client: Redis, user_id: int) -> dict | None:
    value = await redis_client.get(f"user:{user_id}")

    if value is None:
        return None

    return json.loads(value)


async def cache_user(
    redis_client: Redis,
    user_id: int,
    payload: dict,
) -> None:
    await redis_client.set(
        f"user:{user_id}",
        json.dumps(payload),
        ex=300,
    )

工程中建议提前约定缓存键格式,例如:

text 复制代码
项目名:环境:业务:版本:标识
myapp:prod:user:v1:123

更新数据库后,要删除或更新对应缓存。常见模式是 Cache Aside ,读取时先查缓存,未命中再查数据库并回填;写入时先提交数据库事务,再使缓存失效。Redis 的 asyncio 客户端和连接池均由官方 redis-py 提供,并要求在应用退出时显式关闭客户端或连接池。


六、Dockerfile 最佳实践

推荐多阶段构建、非 root 用户、固定 Python 版本和依赖锁文件。不要把开发工具和编译器留在最终镜像里。

dockerfile 复制代码
# syntax=docker/dockerfile:1.7

FROM python:3.12-slim AS builder

ENV UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy

WORKDIR /build

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

COPY pyproject.toml uv.lock ./

RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync \
      --frozen \
      --no-dev \
      --no-install-project


FROM python:3.12-slim AS runtime

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PATH="/app/.venv/bin:$PATH"

RUN groupadd --system --gid 10001 app \
    && useradd --system --uid 10001 --gid app --home-dir /app app

WORKDIR /app

COPY --from=builder --chown=app:app /build/.venv /app/.venv
COPY --chown=app:app app /app/app
COPY --chown=app:app migrations /app/migrations
COPY --chown=app:app alembic.ini /app/alembic.ini
COPY --chown=app:app pyproject.toml /app/pyproject.toml

USER app

EXPOSE 8000

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

生产镜像中应避免:

  • 使用 latest 作为 Python 基础镜像标签
  • 以 root 身份运行服务
  • .env、测试数据或 Git 目录复制进镜像
  • 在容器启动时执行 pip install
  • 开启 --reload
  • 把 PostgreSQL 和 Redis 塞进 API 镜像
  • 将数据库迁移隐式绑定到每个 API 副本的启动过程

镜像构建还可以进一步固定基础镜像 digest。示例里的 uv:latest 为了便于阅读,严谨的生产配置也应锁定版本或 digest。FastAPI 官方的容器指南同样采用从官方 Python 镜像构建、先复制依赖文件以利用缓存、再复制应用代码的方式。

.dockerignore

dockerignore 复制代码
.git
.github
.idea
.vscode

.env
.env.*
!.env.example

__pycache__
*.py[cod]
.pytest_cache
.mypy_cache
.ruff_cache

.venv
dist
build
htmlcov
.coverage
tests

七、Docker Compose 开发环境

基础 Compose 文件可以同时描述应用和依赖,但开发专属的源码挂载、热更新最好放进覆盖文件。

compose.yaml

yaml 复制代码
services:
  api:
    build:
      context: .
      target: runtime
    command:
      - uvicorn
      - app.main:app
      - --host=0.0.0.0
      - --port=8000
      - --proxy-headers
    env_file:
      - .env
    ports:
      - "8000:8000"
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy
    restart: unless-stopped
    init: true
    stop_grace_period: 30s
    networks:
      - backend

  migrate:
    build:
      context: .
      target: runtime
    command: ["alembic", "upgrade", "head"]
    env_file:
      - .env
    depends_on:
      postgres:
        condition: service_healthy
    restart: "no"
    networks:
      - backend

  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 20
      start_period: 5s
    restart: unless-stopped
    networks:
      - backend

  redis:
    image: redis:8
    command:
      - redis-server
      - --appendonly
      - "yes"
      - --maxmemory
      - 256mb
      - --maxmemory-policy
      - allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 20
    restart: unless-stopped
    networks:
      - backend

volumes:
  postgres_data:
  redis_data:

networks:
  backend:

这里没有将 PostgreSQL 和 Redis 端口映射到宿主机,因为 API 可以通过 Compose 内部网络访问它们。确实需要从宿主机连接数据库时,再在开发覆盖文件中开放端口。

compose.dev.yaml

yaml 复制代码
services:
  api:
    command:
      - uvicorn
      - app.main:app
      - --host=0.0.0.0
      - --port=8000
      - --reload
    volumes:
      - ./app:/app/app
      - ./tests:/app/tests
    environment:
      DEBUG: "true"

  postgres:
    ports:
      - "127.0.0.1:5432:5432"

  redis:
    ports:
      - "127.0.0.1:6379:6379"

运行命令:

bash 复制代码
docker compose -f compose.yaml -f compose.dev.yaml up --build

depends_on 只表达依赖关系还不够,数据库进程已经启动并不意味着它已经能接受连接。配合 healthcheckcondition: service_healthy,Compose 才会等待依赖达到健康状态。Docker 官方文档对此有明确说明。


八、数据库迁移

数据库结构变化应由 Alembic 管理,不要在 FastAPI 启动事件中调用 metadata.create_all() 代替正式迁移。

初始化:

bash 复制代码
docker compose run --rm api alembic init migrations

生成迁移:

bash 复制代码
docker compose run --rm api \
  alembic revision --autogenerate -m "create users table"

应用迁移:

bash 复制代码
docker compose run --rm api alembic upgrade head

生产环境建议将迁移作为部署流水线中的独立任务。不建议每个 API 容器启动时都自动迁移,因为多个副本可能同时修改数据库。

对于删除列、修改字段类型一类破坏性变更,可采用兼容式迁移:

  1. 新增字段,但暂不删除旧字段。
  2. 应用同时兼容新旧结构。
  3. 回填历史数据。
  4. 新版本只使用新字段。
  5. 确认无旧版本运行后,再删除旧字段。

这类 expand-and-contract 流程虽然多走两步,却能显著降低滚动发布期间的故障风险。Alembic 是 SQLAlchemy 官方生态中的迁移工具,支持迁移脚本、版本链以及基于 ORM 元数据的自动差异生成。


九、健康检查、日志与监控

健康检查最好拆成两个接口。

存活检查

/health/live 只判断进程是否正常响应,不访问外部依赖。若失败,编排平台可以重启容器。

就绪检查

/health/ready 检查 PostgreSQL、Redis 等关键依赖。失败时停止接收新流量,但不一定立即重启容器。

不要把复杂业务查询塞进健康检查,否则监控本身也可能变成数据库压力来源。

日志建议

  • 日志写入标准输出和标准错误,不写容器内文件。
  • 生产环境采用 JSON 结构化日志。
  • 每个请求携带 request_idtrace_id
  • 避免记录密码、令牌、Cookie 和完整个人信息。
  • 记录请求耗时、状态码和异常类型。
  • 接入 OpenTelemetry、Prometheus、Grafana 或云厂商监控。

推荐至少观察这些指标:

  • 请求量、错误率和延迟分位数
  • PostgreSQL 活跃连接数与连接等待时间
  • 慢查询数量和事务回滚率
  • Redis 命中率、内存占用、过期和驱逐数量
  • 容器 CPU、内存和重启次数
  • 后台任务积压量

十、测试与持续集成

测试应分为三个层次。

层次 内容 是否需要真实依赖
单元测试 Service、纯函数、权限规则 通常不需要
集成测试 Repository、SQL、Redis 行为 需要
API 测试 路由、认证、事务与响应 通常需要

数据库相关测试尽量使用真实 PostgreSQL,而不是用 SQLite 替代。两者在字段类型、约束、JSON、事务和 SQL 语法上的差异,可能让测试产生虚假的安全感。

CI 流程可以设计为:

text 复制代码
Ruff 检查
    ↓
类型检查
    ↓
单元测试
    ↓
启动 PostgreSQL 和 Redis
    ↓
执行 Alembic 迁移
    ↓
集成测试
    ↓
构建镜像
    ↓
镜像漏洞扫描
    ↓
推送镜像仓库

镜像标签建议同时包含 Git 提交号,例如:

text 复制代码
registry.example.com/myapp/api:git-a1b2c3d

部署时使用不可变标签或 digest,不依赖 latest


十一、生产环境需要调整的地方

开发 Compose 不能原封不动搬进生产环境。正式部署至少应完成下面这些调整。

  • PostgreSQL 和 Redis 优先使用托管服务,或建立成熟的备份、恢复和高可用机制。
  • 不对公网暴露数据库与 Redis 端口。
  • 使用 secrets 管理密码和证书。
  • 配置 CPU、内存限制以及合理的重启策略。
  • 在入口层终止 TLS,并配置可信代理。
  • API 镜像以非 root 用户运行,并尽量使用只读文件系统。
  • 定期执行 PostgreSQL 备份和恢复演练。
  • Redis 若只承担缓存,可以接受数据丢失;若承担队列或锁,要明确持久化和故障语义。
  • 后台任务单独运行,不在请求处理函数中执行长时间计算。
  • 对上传文件使用对象存储,不依赖容器本地目录。
  • 在容器收到 SIGTERM 后停止接收新请求,并留出优雅关闭时间。

如果运行在 Kubernetes、ECS 或类似平台,通常采用一个容器一个 Uvicorn worker,通过增加 Pod 或任务数扩容。若只是单台 Linux 服务器,可以在一个容器中设置少量 worker,但必须重新核算数据库连接池。


十二、一套实用的落地顺序

别一上来就铺满 Kubernetes、消息队列和十几个微服务。对多数新项目,更稳妥的演进路径是:

  1. 建立上述目录结构和依赖锁文件。
  2. 用 Compose 启动 FastAPI、PostgreSQL 和 Redis。
  3. 接入 SQLAlchemy 异步会话与 Alembic。
  4. 通过 lifespan 管理 Redis 和数据库资源。
  5. 增加存活与就绪检查。
  6. 配置 Ruff、pytest 和 CI。
  7. 构建非 root、多阶段生产镜像。
  8. 接入结构化日志与监控。
  9. 有明确性能数据后,再调整 worker、连接池和缓存策略。
  10. 业务确实需要时,再引入 Celery、Dramatiq、Arq 或其他任务系统。

这套方案的核心并不花哨------镜像可复现、配置与代码分离、依赖有健康检查、事务边界清晰、迁移独立执行、开发和生产环境明确分层。这些基础打牢之后,项目即使逐渐变大,也不会很快陷入只能祈祷部署成功的阶段。


参考资料

相关推荐
AI绘画哇哒哒10 小时前
【建议收藏!】35岁后端血泪忠告,这3类人别硬转Agent(过来人亲述)
java·人工智能·后端·ai·程序员·大模型·agent
聪明蛋子哟10 小时前
Stagehand v3多语言SDK:Python/Go/Rust/Java下的浏览器自动化统一方案
python·golang·rust
唐青枫11 小时前
一个值多种形态:Zig union、Tagged Union 与内存布局实战
后端
今天AI了吗11 小时前
Python 基础语法(一):常量、变量、输入输出与运算符
开发语言·数据库·人工智能·python·sql·深度学习·机器学习
卷无止境11 小时前
Windows 上丝滑开发 Python,并稳定构建 Docker 镜像
后端·python·docker
Nturmoils11 小时前
备份完不算完,先还原到临时库验一遍
后端
TELL52111 小时前
selenium webdriver 第二次初始化的异常
开发语言·python
Csvn12 小时前
📊 SQL 入门 Day 22:视图与物化视图
后端·sql
Csvn12 小时前
🐍 Day 4: Python 控制流 — 条件、循环与推导式的艺术
后端·python