一套稳健的 FastAPI 工程,不是把 API、Redis 和 PostgreSQL 塞进同一个 Compose 文件就算完事。更合理的做法是把应用设计成无状态服务,数据库迁移、配置管理、健康检查和测试各自承担清晰职责;开发环境追求反馈速度,生产环境则强调镜像可复现、最小权限和可观测性。
下面给出一套可以直接落地的项目骨架。
一、推荐架构
开发环境可以使用 Docker Compose 编排所有依赖,代码通过挂载目录实现热更新。生产环境仍然使用相同镜像,但不再挂载源代码,也不启用 --reload。
各组件的职责建议如下。
| 组件 | 主要职责 | 工程建议 |
|---|---|---|
| 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 官方文档强调,Session 和 AsyncSession 是有状态的事务对象,不应被多个并发任务共享;连接在事务结束后会被释放回 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 只表达依赖关系还不够,数据库进程已经启动并不意味着它已经能接受连接。配合 healthcheck 和 condition: 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 容器启动时都自动迁移,因为多个副本可能同时修改数据库。
对于删除列、修改字段类型一类破坏性变更,可采用兼容式迁移:
- 新增字段,但暂不删除旧字段。
- 应用同时兼容新旧结构。
- 回填历史数据。
- 新版本只使用新字段。
- 确认无旧版本运行后,再删除旧字段。
这类 expand-and-contract 流程虽然多走两步,却能显著降低滚动发布期间的故障风险。Alembic 是 SQLAlchemy 官方生态中的迁移工具,支持迁移脚本、版本链以及基于 ORM 元数据的自动差异生成。
九、健康检查、日志与监控
健康检查最好拆成两个接口。
存活检查
/health/live 只判断进程是否正常响应,不访问外部依赖。若失败,编排平台可以重启容器。
就绪检查
/health/ready 检查 PostgreSQL、Redis 等关键依赖。失败时停止接收新流量,但不一定立即重启容器。
不要把复杂业务查询塞进健康检查,否则监控本身也可能变成数据库压力来源。
日志建议
- 日志写入标准输出和标准错误,不写容器内文件。
- 生产环境采用 JSON 结构化日志。
- 每个请求携带
request_id或trace_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、消息队列和十几个微服务。对多数新项目,更稳妥的演进路径是:
- 建立上述目录结构和依赖锁文件。
- 用 Compose 启动 FastAPI、PostgreSQL 和 Redis。
- 接入 SQLAlchemy 异步会话与 Alembic。
- 通过 lifespan 管理 Redis 和数据库资源。
- 增加存活与就绪检查。
- 配置 Ruff、pytest 和 CI。
- 构建非 root、多阶段生产镜像。
- 接入结构化日志与监控。
- 有明确性能数据后,再调整 worker、连接池和缓存策略。
- 业务确实需要时,再引入 Celery、Dramatiq、Arq 或其他任务系统。
这套方案的核心并不花哨------镜像可复现、配置与代码分离、依赖有健康检查、事务边界清晰、迁移独立执行、开发和生产环境明确分层。这些基础打牢之后,项目即使逐渐变大,也不会很快陷入只能祈祷部署成功的阶段。
参考资料
-
FastAPI Documentation, FastAPI in Containers -- Docker
-
Docker Documentation, Control startup and shutdown order in Compose ;Manage sensitive data with Docker secrets
-
SQLAlchemy Documentation, Asynchronous I/O
-
SQLAlchemy Documentation, Connection Pooling ;Session Basics
-
redis-py Documentation, Asyncio Examples
-
Alembic Documentation, SQLAlchemy Alembic Documentation