把 FastAPI 跑起来只要一条 fastapi dev,但把它放到公网长期运行,问题马上变成另一套:进程怎么重启、数据库什么时候就绪、HTTPS 在哪里终止、迁移失败怎么停住、备份能不能真的恢复。
这篇按单台 Linux VPS 的常见生产结构来部署:FastAPI 和 PostgreSQL 18 由 Docker Compose 管理,应用端口只绑定 127.0.0.1,宿主机 Nginx 负责域名和 HTTPS。示例适合已有 FastAPI 项目上线,也能作为新项目的最小骨架。配置经过静态检查;资源建议不是并发基准,正式业务仍要在自己的接口、数据量和流量模型下压测。
请求路径很简单:
浏览器或客户端 -> 80/443 -> Nginx -> 127.0.0.1:8000 -> FastAPI 容器
-> PostgreSQL 容器
PostgreSQL 不映射宿主机端口,FastAPI 的 8000 也不对公网开放。公网只需要 SSH、HTTP 和 HTTPS。
| 场景 | 建议起点 | Worker 起点 | 需要重点观察 |
|---|---|---|---|
| 测试、低频内部 API | 1–2 vCPU、2GB RAM、30GB SSD | 1 | 构建时内存、磁盘余量 |
| 小型生产 API | 2 vCPU、4GB RAM、50GB SSD | 2 | 每个 Worker 的内存、数据库连接数 |
| CPU 计算或模型推理 | 按任务单独评估 | 先从 1 开始 | CPU、模型内存、请求超时 |
Worker 不是越多越好。FastAPI 官方部署文档提醒,每个进程都有自己的内存;如果应用启动后占用 500MB,4 个 Worker 可能仅应用进程就接近 2GB。数据库连接池也会随进程倍增。先从 1–2 个 Worker 开始,用真实请求观察,再调整。
准备条件:
- Ubuntu 24.04 或同类受支持 Linux;
- 已安装 Docker Engine 和 Compose 插件;
- 一个已经解析到 VPS 公网 IP 的域名,例如
api.example.com; - 云防火墙和 UFW 允许 22、80、443,未开放 8000、5432;
- FastAPI 项目有
/healthz健康检查,并用 Alembic 管理数据库迁移。
Docker 尚未安装时,先按 VPS Docker 部署教程完成安装。本文目录统一为 /opt/fastapi-app。
先建立只允许管理员写入的部署目录:
sudo install -d -o root -g root -m 755 /opt/fastapi-app
cd /opt/fastapi-app
sudo install -d -o root -g root -m 700 backups
推荐目录结构:
/opt/fastapi-app/
├── app/
│ ├── __init__.py
│ └── main.py
├── alembic/
├── alembic.ini
├── compose.yaml
├── Dockerfile
├── .dockerignore
├── .env
└── requirements.txt
FastAPI 官方现在建议从官方 Python 镜像构建自己的镜像,旧的 tiangolo/uvicorn-gunicorn-fastapi 基础镜像已经弃用。依赖也不要长期漂移。若项目使用 uv,把精确版本保存在 uv.lock,构建前导出:
uv add "fastapi[standard]" sqlalchemy "psycopg[binary]" alembic pydantic-settings
uv export --format requirements-txt --no-dev --no-emit-project \
--output-file requirements.txt
已有 Poetry、pip-tools 或其他锁文件就继续使用原工具,不必为了部署更换依赖管理器。关键是把锁文件纳入版本控制,并在升级依赖后运行测试。
应用至少提供一个不依赖外部第三方服务的存活接口:
from fastapi import FastAPI
app = FastAPI()
@app.get("/healthz", include_in_schema=False)
def healthz() -> dict[str, str]:
return {"status": "ok"}
存活检查不应执行耗时查询。数据库可用性可以另做 /readyz,但不要把敏感异常、连接串或堆栈返回给公网。
把下面内容保存为 /opt/fastapi-app/Dockerfile:
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
RUN groupadd --system --gid 10001 app \
&& useradd --system --uid 10001 --gid app --home-dir /app app
WORKDIR /app
COPY requirements.txt ./requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY --chown=app:app . .
USER app
EXPOSE 8000
CMD ["fastapi", "run", "app/main.py", "--host", "0.0.0.0", "--port", "8000"]
如果项目依赖需要编译器,使用多阶段构建,不要把 gcc、头文件和包管理器缓存留在运行镜像。基础镜像也要定期更新,正式发布最好记录镜像 digest 和源码 commit。
.dockerignore 至少排除这些内容:
.git
.env
__pycache__/
*.py[cod]
.pytest_cache/
.venv/
backups/
.env 被排除后,不会意外复制进镜像;运行时由 Compose 传入。
先生成数据库密码。只对全新部署执行一次:
cd /opt/fastapi-app
umask 077
db_password=$(openssl rand -base64 36 | tr -d '\n')
cat > .env.partial <<EOF
POSTGRES_DB=fastapi
POSTGRES_USER=fastapi
POSTGRES_PASSWORD=${db_password}
WEB_CONCURRENCY=2
EOF
mv .env.partial .env
chmod 600 .env
unset db_password
.env 不要提交到 Git,也不要把真实密码贴进工单。应用最好从 POSTGRES_HOST、POSTGRES_DB、POSTGRES_USER、POSTGRES_PASSWORD 等独立变量构造 SQLAlchemy URL,避免密码中的 @、:、/ 被误当成 URL 语法。
保存 /opt/fastapi-app/compose.yaml:
name: fastapi-production
x-app: &app
image: fastapi-app:local
build:
context: .
env_file:
- .env
environment:
POSTGRES_HOST: db
POSTGRES_PORT: "5432"
depends_on:
db:
condition: service_healthy
networks:
- backend
services:
db:
image: postgres:18-alpine
restart: unless-stopped
stop_grace_period: 1m
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
migrate:
<<: *app
command: ["alembic", "upgrade", "head"]
restart: "no"
profiles: ["tools"]
app:
<<: *app
restart: unless-stopped
init: true
command:
- fastapi
- run
- app/main.py
- --host
- 0.0.0.0
- --port
- "8000"
- --workers
- "${WEB_CONCURRENCY:-2}"
- --proxy-headers
- --forwarded-allow-ips=*
ports:
- "127.0.0.1:8000:8000"
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=3)"
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
backend:
volumes:
postgres_data:
Docker Compose 的普通 depends_on 只保证容器启动顺序,不保证数据库已经能接收连接。这里使用 service_healthy,等 pg_isready 成功后再运行迁移或应用。
--forwarded-allow-ips=* 允许 FastAPI 接收 Nginx 转发的协议和客户端信息。本例的 8000 只绑定宿主机回环地址,后端网络也不要接入不可信容器;如果以后把端口暴露到公网,这个设置必须一起收紧。
如果程序确实要写本地文件,不要直接取消 read_only。把上传目录、缓存目录或临时目录作为单独 volume 挂载,并明确所有权、容量和备份策略。
先解析配置并构建镜像:
cd /opt/fastapi-app
docker compose config --quiet
docker compose build --pull app
docker compose up -d db
docker compose ps
docker compose logs --tail=100 db
数据库显示 healthy 后,单独执行迁移:
docker compose --profile tools run --rm migrate
docker compose up -d app
docker compose ps
docker compose logs --tail=100 app
curl --fail --show-error http://127.0.0.1:8000/healthz
docker image inspect fastapi-app:local --format '{{json .RepoDigests}}'
不要让每个 Worker 同时自动执行 alembic upgrade head。迁移是一次性发布步骤,多进程竞争可能造成锁等待或重复操作。Alembic 成功返回后再启动应用,失败就保留旧应用版本并检查迁移日志。
生产发布最好给应用镜像加不可变标签,例如 Git commit SHA,而不是长期只用 local 或 latest。数据库大版本也不能靠改 postgres:18-alpine 为下一个主版本直接升级;需要按 PostgreSQL 的升级流程测试迁移。
在宿主机安装 Nginx 与 Certbot:
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw status verbose
确认云防火墙同样只开放必要端口。把域名替换为自己的真实域名,保存 /etc/nginx/conf.d/fastapi.conf:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name api.example.com;
client_max_body_size 10m;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
检查语法后再重载:
sudo nginx -t
sudo systemctl reload nginx
curl --fail --show-error http://api.example.com/healthz
sudo certbot --nginx -d api.example.com
sudo certbot renew --dry-run
curl --fail --show-error https://api.example.com/healthz
Certbot 会修改 Nginx 配置并创建 HTTPS 监听。执行前要确保域名 A/AAAA 记录都指向这台 VPS,80 和 443 能从公网访问。若 AAAA 记录指向错误的 IPv6 地址,验证经常会失败。
proxy_read_timeout 不是处理超长任务的万能开关。需要几分钟的导出、视频处理或模型任务,应改为队列加状态查询;把超时无限拉长只会占住连接和 Worker。
接口上传文件时,Nginx 的 client_max_body_size、应用自身限制和上游 CDN 限制必须一起检查。安全响应头、TLS 和限速可继续参考 VPS Nginx 安全加固指南。
关闭 /docs 和 /openapi.json 只能减少信息暴露,不能代替接口认证。管理接口仍要使用 OAuth2、短期令牌、mTLS 或其他合适的认证,并在应用层做权限判断。
浏览器前端跨域访问时,只允许真实前端域名、必要方法和请求头。CORS 不是防火墙,也不会阻止 curl 或服务端请求;敏感接口仍要鉴权和限流。
迁移账号通常需要建表、改表权限,运行账号只需要业务读写权限。项目初期可以先共用,但上线后应按实际 SQL 拆分。PostgreSQL 5432 不要映射到公网;临时管理优先使用 SSH 隧道。
.env 权限保持 600。云 API Key、JWT 签名密钥、邮件密码和数据库密码都应独立生成,不能复用。Docker Compose 的 env_file 不是密钥保险箱,拥有 root 或 Docker 权限的人仍可读取。
下面用 PostgreSQL 自定义格式导出,便于 pg_restore 查看和选择对象。保存为 /opt/fastapi-app/backup.sh:
#!/usr/bin/env bash
set -euo pipefail
umask 077
cd /opt/fastapi-app
backup_stamp=$(date -u +%Y%m%dT%H%M%SZ)
backup_file="backups/fastapi-${backup_stamp}.dump"
partial_file="${backup_file}.partial"
test ! -e "$backup_file"
test ! -e "$partial_file"
docker compose exec -T db sh -c \
'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --format=custom --no-owner' \
> "$partial_file"
test -s "$partial_file"
docker compose exec -T db pg_restore --list < "$partial_file" > /dev/null
mv "$partial_file" "$backup_file"
sha256sum "$backup_file" > "${backup_file}.sha256"
printf 'Backup ready: %s\n' "$backup_file"
执行并检查:
chmod 700 /opt/fastapi-app/backup.sh
/opt/fastapi-app/backup.sh
ls -lh /opt/fastapi-app/backups
这份备份只包含当前业务数据库,不包含 .env、Nginx 配置、上传文件和镜像标签。它们要纳入单独的加密备份。完成文件与 .sha256 应复制到异地存储;同一块 VPS 磁盘上的备份无法应对整机损坏或账号失陷。
每天一份备份最多可能丢失接近一天的数据。更短的恢复点目标需要增加备份频率或设计 WAL 归档。备份保留、加密和异地复制可参考 VPS 备份恢复演练指南。
下面把备份恢复到同一 PostgreSQL 实例中的临时数据库,能验证文件结构、对象和基础查询,但不等于整机灾难恢复。生产库很大时应在独立 VPS 或隔离实例演练,避免占满生产磁盘和 I/O。
cd /opt/fastapi-app
backup_file='backups/fastapi-YYYYMMDDTHHMMSSZ.dump'
restore_db="restore_$(date -u +%Y%m%d%H%M%S)"
sha256sum -c "${backup_file}.sha256"
docker compose exec -T db pg_restore --list < "$backup_file" > /dev/null
docker compose exec -T db sh -c \
'createdb -U "$POSTGRES_USER" "'$restore_db'"'
docker compose exec -T db sh -c \
'pg_restore -U "$POSTGRES_USER" -d "'$restore_db'" --no-owner --exit-on-error' \
< "$backup_file"
docker compose exec -T db sh -c \
'psql -U "$POSTGRES_USER" -d "'$restore_db'" -c "\\dt"'
再按业务挑几张关键表检查行数、唯一约束和最近数据。如果应用使用 Alembic,也要确认临时库中的 alembic_version。验证完成后,先复核变量只指向临时库,再删除:
printf 'temporary database: %s\n' "$restore_db"
docker compose exec -T db sh -c \
'dropdb -U "$POSTGRES_USER" "'$restore_db'"'
unset restore_db
不要把示例文件名原样执行,也不要对生产数据库使用 pg_restore --clean。真正的恢复预案还要记录 DNS、证书、上传文件、环境变量、镜像版本和恢复顺序。
一次安全发布应把代码、依赖和数据库迁移拆开观察:
cd /opt/fastapi-app
/opt/fastapi-app/backup.sh
docker compose build --pull app
docker compose --profile tools run --rm migrate
docker compose up -d app
docker compose ps
docker compose logs --tail=100 app
curl --fail --show-error https://api.example.com/healthz
发布前先在测试环境运行单元测试、接口测试和迁移。应用镜像应带 commit 标签并保留上一个已验证版本。若新版本失败,可以切回旧镜像;但已经执行的数据库迁移未必能安全降级,所以迁移必须设计为向后兼容,破坏性删列应放到后续发布。
更新 PostgreSQL 小版本前先备份并做恢复演练;跨主版本升级是独立项目。也不要用 docker compose down -v 排障,它会删除声明的数据卷。
先分层检查,不要反复重启:
curl -v http://127.0.0.1:8000/healthz
docker compose ps
docker compose logs --tail=200 app
sudo tail -n 100 /var/log/nginx/error.log
sudo ss -lntp 'sport = :8000'
本机 8000 都不通,问题在应用、容器或端口绑定;本机通而域名 502,再看 Nginx upstream、SELinux/AppArmor 和配置文件。更完整的路径见 VPS 502/504 排查清单。
docker compose ps
docker compose logs --tail=200 db
docker compose exec -T db sh -c \
'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
docker compose exec -T app getent hosts db
容器内数据库主机应写 db,不是 127.0.0.1。如果改过 .env 密码,已有数据卷里的 PostgreSQL 账号密码不会自动跟着变;需要在数据库中显式修改,并同步应用配置。
确认 Nginx 传递 X-Forwarded-Proto,应用启用了代理头,并且 8000 没有暴露给不可信来源。若 API 挂在 /api 子路径,还要正确配置 FastAPI root_path 和 Nginx 路径转发;能用独立子域名时通常更省事。
检查 OOM 和连接数:
docker stats --no-stream
journalctl -k --since '1 hour ago' | grep -i -E 'oom|killed process'
docker compose exec -T db sh -c \
'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select count(*) from pg_stat_activity;"'
每个 Worker 都会加载应用并创建连接池。内存不够就减少 Worker,连接过多就收紧每个进程的池大小;不要只把 PostgreSQL max_connections 调大。
- FastAPI 容器部署文档:官方 Python 镜像、依赖锁、Worker 和旧基础镜像弃用说明;
- Docker Compose 启动顺序:
service_healthy与健康检查; - Nginx 反向代理模块:
proxy_pass与代理请求头; - Certbot Nginx 使用指南:证书申请和续期;
- PostgreSQL pg_dump 文档:逻辑备份行为与限制;
- Alembic 教程:迁移环境和
upgrade head。
先在测试域名完成一次“构建、迁移、健康检查、备份、恢复”闭环,再切生产 DNS。能恢复的备份、可回退的镜像和不会并发执行的迁移,才是这套 FastAPI 部署真正可长期维护的部分。
