把 Python/FastAPI 后端部署到 VPS:Gunicorn + Nginx + systemd
2026-08-15 · DevCraft Studio
写好的 FastAPI 怎么上线?Gunicorn 多 worker、Nginx 反代、systemd 托管,再加虚拟环境与依赖锁定的标准姿势。
FastAPI 写起来是真爽:类型提示、自动文档、异步一把梭。可写完 uvicorn main:app --reload 在本地跑通之后,很多人卡在了下一步——这东西怎么弄到 VPS 上,让它在公网稳稳地跑,还能开机自启、崩溃自愈?直接把开发服务器丢上公网是最典型的翻车现场:单进程、不处理 TLS、慢客户端能拖死整个 worker、机器一重启就没人管。正确的生产姿势是三层:虚拟环境加依赖锁定保证“跑的是同一份代码”、Gunicorn 起多个 worker 并用 systemd 托管、Nginx 在前面做反代和证书。本文一步一步给出可照搬的命令。
很多团队在容器流行之前就靠这套组合稳跑了好几年,它不依赖 Docker,也不依赖某个云厂商的 PaaS,只要你有一台能 SSH 的 Linux 机器就能落地。也正是因为这个原因,它特别适合预算有限、想自己掌控全过程的小项目和个人站点。你可以把它理解成“穷人版的 Kubernetes”:没有编排和自动扩缩,但进程守护、开机自启、日志归集、反代证书这些生产刚需一个不少,而学习和维护成本只有容器方案的一个零头。
延伸阅读
更多相关攻略推荐:Ollama AI系列(2):VPS上的AI推理与API应用、Ollama AI系列(2):VPS上的AI推理与API应用、ARM / Ampere 席卷 VPS:性价比真香还是兼容陷阱?、【VPS 硬件选型指南 (内存 篇) 02】大内存 VPS 能干嘛?、2026 独立站 PCI-DSS 自查清单:SAQ A 还是 A-E。
一、虚拟环境 + 依赖锁定:让生产跑的就是你测的那份
生产部署的第一原则是可复现。永远不要拿系统 Python 直接 pip install,那样哪天系统升级把依赖冲掉,或者服务之间互相污染,排查能让你崩溃。先在项目目录建一个虚拟环境:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt关键是 requirements.txt 必须锁版本。手动写容易漏,推荐用 pip freeze 导出,或者更讲究一点用 Poetry。Poetry 的 pyproject.toml + poetry.lock 能同时锁住直接和传递依赖,CI 里 poetry install --no-dev 装出来的环境和你本地完全一致:
# 用 Poetry 时导出锁定文件
poetry lock
poetry install --no-dev
# 或者传统方式锁定
pip freeze > requirements.txt踩坑提醒:部署用 pip install -r requirements.txt 就好,别用 pip install 不带清单去“装最新版”,也别把开发依赖(pytest、black 之类)带进生产。Gunicorn 和 uvicorn 属于运行时依赖,必须进清单。把 venv 目录整体交给运行用户,权限收紧,开发机和线上就是同一套环境,少掉八成“我本地是好的”类问题。
二、Gunicorn + UvicornWorker:把多核用起来
FastAPI 是 ASGI 框架,不能直接用老式 WSGI 的 Gunicorn 默认 worker,得让它用 UvicornWorker 作为 worker 类,由 Gunicorn 当进程管理器、Uvicorn 负责处理异步请求。最小可用的一行:
gunicorn main:app -k uvicorn.workers.UvicornWorker --workers 4 --bind 0.0.0.0:8000 --timeout 120worker 数怎么定?官方经验公式是 (2 × CPU核心数) + 1。四核机器大概 9 个,但这只是起点:I/O 密集(大量数据库查询、外部 API 调用)可以照这个公式,CPU 密集(推理、图像处理)反而该收紧到核心数,把重活丢给后台任务。别把 worker 数当成“并发客户端数”,那是线程和异步该操心的事,开几百个 worker 只会因上下文切换把吞吐拖垮。timeout 默认 30 秒是个坑:它不是“请求时长上限”的 HTTP 语义,而是“worker 超过这么久没心跳就被杀掉重启”。一个合法但要跑 35 秒的接口,在 sync worker 下会被活活掐断返回 502。把它调到高于你最慢的合法接口(通常 120 秒),但更慢的活该丢进 Celery/RQ 后台队列,而不是靠调大 timeout 掩盖。
如果你用 Poetry 管理依赖,systemd 的 ExecStart 也可以写成 poetry run gunicorn main:app -c gunicorn_conf.py,Poetry 会自己激活虚拟环境再启动,unit 文件里就不需要手动拼 PATH。但要注意,poetry run 会多一层启动开销,而且要求 Poetry 本身装在系统里、对运行用户可达。对于“少一个外部依赖是一条原则”的场景,还是推荐直接用 venv 里的 gunicorn 绝对路径,少一层抽象就少一处出错的可能。
# gunicorn_conf.py
import multiprocessing
bind = "unix:/run/myapi.sock"
worker_class = "uvicorn.workers.UvicornWorker"
workers = (multiprocessing.cpu_count() * 2) + 1
timeout = 120
keepalive = 5
max_requests = 1000
max_requests_jitter = 50
errorlog = "/var/log/myapi/error.log"
accesslog = "/var/log/myapi/access.log"两个值得记的参数:max_requests 加 max_requests_jitter 是内存泄漏的保险——每个 worker 服务满约 1000 个请求就优雅重启,专治你控制不了的那个依赖的慢漏;bind 到 Unix socket 而不是 TCP 端口,能省掉本地网络开销,也顺带让防火墙规则更干净(外部根本连不到 8000)。
三、systemd 托管:开机自启、崩溃自愈、日志进 journald
别用 tmux 挂着 Gunicorn 凑合。systemd 免费给你自动重启、日志归集、依赖顺序和资源限制。写一个 unit 文件,把 socket 路径、虚拟环境、运行用户都定死:
# /etc/systemd/system/myapi.service
[Unit]
Description=FastAPI Application
After=network.target
[Service]
Type=notify
User=deploy
Group=www-data
WorkingDirectory=/var/www/api
Environment="PATH=/var/www/api/venv/bin:/usr/bin"
EnvironmentFile=/var/www/api/.env
ExecStart=/var/www/api/venv/bin/gunicorn main:app -c gunicorn_conf.py
ExecReload=/bin/kill -s HUP $MAINPID
Restart=always
RestartSec=5
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/www/api/storage /var/log/api
[Install]
WantedBy=multi-user.target几个要点:EnvironmentFile 加载 .env,密钥不用写进 unit 文件本体;Restart=always 保证崩溃即拉起;Type=notify 配合 Gunicorn 就绪信号,systemd 知道它真的起来了而不是假活。PrivateTmp 和 ProtectSystem=strict 是零成本的加固,把进程能乱写的目录限死,真出事爆炸半径小一圈。写完三步走:
sudo systemctl daemon-reload
sudo systemctl enable myapi
sudo systemctl start myapi
sudo journalctl -u myapi -f发布更新时,推完代码再 sudo systemctl reload myapi,它给 Gunicorn 发 HUP,worker 优雅替换、连接不丢。如果 socket 文件权限不对,Nginx 会连不上,记得确认 www-data 能读到 /run/myapi.sock。
刚部署完别急着走人,先 curl 一下本地 socket:不通就去看 journalctl 的报错,十有八九是 PATH 没指到 venv,或者 .env 里少了某个必填变量。把 Type=notify 配上之后,systemctl is-active myapi 显示 active 才算真起来,而不是“进程起了但 Gunicorn 还没 bind 上 socket”的假活。这种启动时延一秒半秒的假活,恰恰是很多“重启后服务起不来”的真正原因。
四、Nginx 反代 + 静态/媒体分离
和 Node 那套一样,Gunicorn 不该直接裸奔公网。Nginx 负责 TLS、压缩、缓冲慢客户端,还有一件 Python 特别该做的:把静态文件和媒体文件从应用里摘出来自己直出。FastAPI 本身不擅长伺候图片、下载包这类大文件,让 Nginx 用 try_files 或 alias 直出,worker 才不会被慢下载占着:
upstream api_backend {
server unix:/run/myapi.sock fail_timeout=0;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
client_max_body_size 10M;
proxy_read_timeout 120s;
location /static/ {
alias /var/www/api/static/;
expires 7d;
access_log off;
}
location /media/ {
alias /var/www/api/media/;
internal;
}
location / {
proxy_pass http://api_backend;
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;
}
}/static/ 直接由 Nginx 带长缓存吐出,/media/ 用 internal 限制只能由后端应用内部重定向访问,避免别人枚举下载你的私有文件。先用 certbot --nginx 把证书和 80→443 跳转一次配好。proxy_read_timeout 对齐 Gunicorn 的 timeout,否则长请求会被 Nginx 先掐。生产环境还建议把 FastAPI 自带的 /docs 和 /redoc 用 Basic Auth 挡一下,或者干脆在路由里禁用,别把接口结构白白暴露给全网。
五、上线检查清单
串起来,一份能交付的 FastAPI 生产部署长这样:venv 隔离 + requirements/lock 锁版本 → Gunicorn 多 worker 绑 Unix socket → systemd 托管自启自愈 → Nginx 反代并分离静态/媒体 → ufw 只开 22 和 80/443 → .env 权限 600 且不进仓库。最后补两件事:加一个 GET /health 健康检查(只回 status,不吐数据库密码之类的内部信息),以及给登录类接口加限流,挡掉暴力破解和撞库。性能调优上别迷信 worker 越多越好。Gunicorn 官方明确说通常 4 到 12 个 worker 就足以扛住很重的流量,超过这个数不但不会更快,反而因进程间争抢和内存翻倍拖慢整体。真正决定吞吐的,往往是数据库慢查询、缺失索引或下游第三方 API 的延迟,而不是 worker 数量。上线后用 journalctl 看应用日志、用 Nginx 的 access log 看响应时间分布,先把最慢的那 1% 接口优化掉,比盲目加 worker 管用得多。
做到这些,你的 FastAPI 就不再是“本地能跑”,而是真正能扛流量的服务了。
延伸阅读:VPS 新手入门指南 帮你走完买机后的第一步;VPS 安全基线 把 ufw 防火墙、SSH 密钥与 fail2ban 讲清楚;用 Docker 自托管服务栈 则是想走容器化路线时的下一步。