这篇解决一个具体问题:把本地跑得好好的 FastAPI 应用,稳定地跑在一台公网服务器上,能被外网访问、进程挂了能自动拉起、日志能查、重启机器后服务还在。跟着做完,你会得到一个 Nginx + Gunicorn + Uvicorn + systemd 的最小可用生产配置。
踩了 5 次坑才跑通,下面所有命令和配置我都亲测过,版本号写死在文中,你照抄能复现。
环境与版本说明
先把环境钉死,避免版本差异导致行为不一致。
| 组件 | 版本 | 说明 |
|---|---|---|
| 服务器系统 | Ubuntu 22.04.3 LTS | 云厂商默认镜像 |
| Python | 3.10.12 | 系统自带,未用 conda |
| FastAPI | 0.110.0 | pip 安装 |
| Uvicorn | 0.27.1 | 带 standard 扩展 |
| Gunicorn | 21.2.0 | pip 安装 |
| Nginx | 1.18.0 | apt 安装 |
| systemd | 249 | 系统自带 |
【踩坑提醒】不要用系统 Python 直接 pip install 装依赖 。Ubuntu 22.04 的 Python 3.10 受 PEP 668 保护,直接 pip 装包会报 externally-managed-environment。我用 venv 隔离,下面会讲。
我的项目结构长这样,你可以对着改:
text
/opt/myapp/
├── venv/ # 虚拟环境
├── app/
│ ├── __init__.py
│ └── main.py # FastAPI 入口
├── requirements.txt
└── .env
问题复现:本机 uvicorn 跑得好,上线就出问题

本地我一般这么起服务:
bash
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
本地没问题。但直接把这个命令丢到服务器上,会遇到三个真实问题:
--reload是开发模式,文件一多会占大量内存,且不适合生产。- 单进程 Uvicorn 只能用一个 CPU 核,多核机器浪费。
- 关掉 SSH 终端,进程就没了;机器重启后也不会自动拉起。
所以我需要的是:多进程 + 进程守护 + 反向代理。方案选型如下。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 裸 uvicorn | 简单,一条命令 | 单进程、无守护、不能多核 | 本地开发 |
| uvicorn --workers N | 配置简单,多进程 | 官方文档不推荐用于生产,缺进程管理 | 小流量内部服务 |
| Gunicorn + UvicornWorker | 成熟进程管理,多 worker,社区主流 | 配置稍多,需注意 worker 类型 | 生产环境推荐 |
| Docker + K8s | 隔离好,易扩缩容 | 运维成本高,小项目过重 | 多服务、团队协作 |
我选 Gunicorn + UvicornWorker,因为它是目前 FastAPI 官方文档推荐的组合之一,进程管理成熟,配置量可控。
排查过程:一步步把环境搭起来
1. 建虚拟环境并装依赖
bash
# 更新包索引
sudo apt update
# 装 venv 和编译工具(有些包要编译)
sudo apt install -y python3-venv python3-pip build-essential
# 建项目目录
sudo mkdir -p /opt/myapp
sudo chown -R $USER:$USER /opt/myapp
cd /opt/myapp
# 建虚拟环境
python3 -m venv venv
# 激活
source venv/bin/activate
# 装依赖,版本写死
pip install fastapi==0.110.0 "uvicorn[standard]==0.27.1" gunicorn==21.2.0
【踩坑提醒】uvicorn[standard] 里的方括号在 shell 里可能被当成通配符,加引号最保险。我第一次没加引号,装成了不含 standard 扩展的版本,结果 websocket 相关的依赖没装上。
把依赖导出成文件,方便重装:
bash
pip freeze > requirements.txt
2. 写一个最小的 FastAPI 应用
/opt/myapp/app/main.py:
python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
# 返回一个简单 JSON,用来验证服务是否通
return {"msg": "hello from production"}
@app.get("/health")
def health():
# 健康检查端点,给负载均衡/监控用
return {"status": "ok"}
3. 先用 Gunicorn 本地拉起,确认能跑
在项目根目录执行:
bash
cd /opt/myapp
source venv/bin/activate
gunicorn app.main:app \
--workers 2 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 127.0.0.1:8000
参数解释:
--workers 2:起 2 个 worker 进程。--worker-class uvicorn.workers.UvicornWorker:让 Gunicorn 用 Uvicorn 的 worker 类型,这样才支持 ASGI。--bind 127.0.0.1:8000:只监听本地,外网访问交给 Nginx。
【踩坑提醒】worker 数量不是越多越好 。我一开始设成 8,结果内存直接飙到 1.5G。参考 Gunicorn 官方建议,同步 worker 用 2 * CPU核数 + 1,但 UvicornWorker 是异步的,我实测下来 2~4 个就够。这台机器 2 核,我用 2 个。这一点没有严格的官方公式,按你实际压测调整。
跑起来后另开一个终端验证:
bash
curl http://127.0.0.1:8000/health
# 期望输出:{"status":"ok"}
如果这里报 ModuleNotFoundError,八成是 app.main:app 的路径写错,或者 app/__init__.py 没建。我第一次就漏了 __init__.py。
4. 用 systemd 托管进程
Gunicorn 直接跑在终端里,SSH 一断就没了。用 systemd 托管。
新建 /etc/systemd/system/myapp.service:
ini
[Unit]
Description=Gunicorn instance to serve myapp
After=network.target
[Service]
# 运行用户,别用 root
User=www-data
Group=www-data
# 工作目录
WorkingDirectory=/opt/myapp
# 关键:用 venv 里的 gunicorn,否则找不到依赖
ExecStart=/opt/myapp/venv/bin/gunicorn app.main:app \
--workers 2 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 127.0.0.1:8000 \
--access-logfile - \
--error-logfile -
# 进程挂了自动重启
Restart=always
# 重启间隔
RestartSec=3
[Install]
WantedBy=multi-user.target
【踩坑提醒】ExecStart 必须写 venv 里 gunicorn 的绝对路径 。我一开始写的是 gunicorn,systemd 找不到,报 status=203/EXEC。因为 systemd 不读你 shell 的 PATH。
还有一点,User=www-data 意味着 /opt/myapp 目录 www-data 得有读权限。我建目录时 chown 给了自己,systemd 起不来,报权限错误。改成:
bash
sudo chown -R www-data:www-data /opt/myapp
重新加载并启动:
bash
sudo systemctl daemon-reload
sudo systemctl start myapp
sudo systemctl enable myapp # 开机自启
sudo systemctl status myapp # 看状态
status 里如果显示 active (running),说明进程起来了。再 curl 一次本地 8000,确认还能通。
5. 配置 Nginx 反向代理
Gunicorn 只监听 127.0.0.1,外网访问不了。Nginx 负责把 80 端口的请求转给 8000。
新建 /etc/nginx/sites-available/myapp:
nginx
server {
listen 80;
server_name your_domain_or_ip;
# 静态文件交给 Nginx 直接返回,别走后端
location /static/ {
alias /opt/myapp/static/;
}
# 其余请求转给 Gunicorn
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
server_name 填你的域名或公网 IP。没有域名就填 IP。
启用配置:
bash
# 建软链接到 sites-enabled
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
# 检查语法
sudo nginx -t
# 重载
sudo systemctl reload nginx
nginx -t 输出 syntax is ok 和 test is successful 才算过。
【踩坑提醒】默认站点会抢 80 端口 。Ubuntu 的 Nginx 装完有个 /etc/nginx/sites-enabled/default,监听 80。如果它还在,你访问 IP 看到的是 Nginx 欢迎页,不是你的应用。删掉它:
bash
sudo rm /etc/nginx/sites-enabled/default
sudo systemctl reload nginx
我第一次就是被这个默认站点坑了,排查了半小时以为反代没生效。
根因定位:几个典型报错的排查路径

部署过程里最常见两类错误,我把排查路径列出来。
502 Bad Gateway
Nginx 返回 502,说明它连不上后端。按顺序查:
sudo systemctl status myapp------ 看 Gunicorn 是不是挂了。curl http://127.0.0.1:8000/health------ 后端自己在不在。sudo journalctl -u myapp -n 50------ 看 Gunicorn 报什么错。- 检查 Nginx 配置里
proxy_pass的端口和 Gunicorn--bind的端口是否一致。
我遇到一次 502,最后发现是 WorkingDirectory 写错,Gunicorn 找不到 app.main 模块。日志里明确写着 ModuleNotFoundError。
静态文件 404
location /static/ 的 alias 路径结尾有没有 / 影响很大。alias /opt/myapp/static/; 结尾带斜杠是对的。如果写成 alias /opt/myapp/static;(不带斜杠),访问 /static/logo.png 会被映射到 /opt/myapp/staticlogo.png,必然 404。这个坑我查了 Nginx 文档才搞明白。
分步解决方案汇总

把上面流程整理成可重复执行的步骤:
apt装python3-venv、build-essential、nginx。- 建
/opt/myapp,建 venv,装固定版本依赖。 - 写
app/main.py,本地用 Gunicorn 拉起验证。 - 写 systemd unit,
daemon-reload后start+enable。 - 写 Nginx server 块,软链到
sites-enabled,删默认站点,nginx -t后reload。 - 外网访问 IP 或域名,验证
/和/health。 - 配 HTTPS(见下文拓展)。
完整可运行代码
把关键文件再完整贴一遍,方便你直接抄。
/opt/myapp/app/main.py:
python
from fastapi import FastAPI
# 创建应用实例
app = FastAPI(title="myapp")
@app.get("/")
def read_root():
return {"msg": "hello from production"}
@app.get("/health")
def health():
# 供 systemd/监控探活
return {"status": "ok"}
/etc/systemd/system/myapp.service:
ini
[Unit]
Description=Gunicorn instance to serve myapp
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/gunicorn app.main:app --workers 2 --worker-class uvicorn.workers.UvicornWorker --bind 127.0.0.1:8000 --access-logfile - --error-logfile -
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
/etc/nginx/sites-available/myapp:
nginx
server {
listen 80;
server_name your_domain_or_ip;
location /static/ {
alias /opt/myapp/static/;
}
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
验证结果
全部配好后,我在本地机器上跑:
bash
curl http://<服务器公网IP>/health
返回:
json
{"status":"ok"}
再 sudo reboot 重启服务器,等 1 分钟,重新 curl,依然返回 {"status":"ok"}。说明 systemd 的开机自启生效了。
sudo systemctl status myapp 显示:
text
● myapp.service - Gunicorn instance to serve myapp
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; ...)
Active: active (running) since ...
enabled 表示开机自启已开。
日志方面,sudo journalctl -u myapp -f 能实时看 Gunicorn 输出。Nginx 日志在 /var/log/nginx/access.log 和 error.log。
适用场景 + 避坑提醒 + 拓展方向

适用场景:单机部署中小流量 FastAPI 服务,没有容器化需求,希望用系统原生工具管理。这套配置在一台 2 核 4G 的机器上跑一个 API 服务绰绰有余。
避坑提醒汇总:
- venv 隔离,别用系统 Python 直接装包。
ExecStart用 venv 里 gunicorn 的绝对路径。/opt/myapp权限要给www-data。- 删掉 Nginx 默认站点。
alias路径结尾带斜杠。- worker 数量按实际压测调,别照搬公式。
拓展方向:
- HTTPS:用
certbot --nginx自动申请 Let's Encrypt 证书,改 443 监听。这一步我还没在这台机器上做,具体步骤以 certbot 官方文档为准。 - 日志切割:Gunicorn 的 access log 会一直涨,可以配 logrotate,或者用
--access-logfile /var/log/myapp/access.log配合系统的 logrotate。 - 数据库连接池:Gunicorn 多 worker 时,每个 worker 是独立进程,数据库连接池要按 worker 数配置,否则总连接数会超。这一点我没在本文展开。
- 容器化:如果以后要上多机,可以考虑 Docker,但那是另一个话题。
有问题评论区留言,看到都会回。