FastAPI 从本机到生产服务器:Nginx+Gunicorn+Uvicorn 完整部署实录

这篇解决一个具体问题:把本地跑得好好的 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

本地没问题。但直接把这个命令丢到服务器上,会遇到三个真实问题:

  1. --reload 是开发模式,文件一多会占大量内存,且不适合生产。
  2. 单进程 Uvicorn 只能用一个 CPU 核,多核机器浪费。
  3. 关掉 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 oktest 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,说明它连不上后端。按顺序查:

  1. sudo systemctl status myapp ------ 看 Gunicorn 是不是挂了。
  2. curl http://127.0.0.1:8000/health ------ 后端自己在不在。
  3. sudo journalctl -u myapp -n 50 ------ 看 Gunicorn 报什么错。
  4. 检查 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 文档才搞明白。

分步解决方案汇总

把上面流程整理成可重复执行的步骤:

  1. aptpython3-venvbuild-essentialnginx
  2. /opt/myapp,建 venv,装固定版本依赖。
  3. app/main.py,本地用 Gunicorn 拉起验证。
  4. 写 systemd unit,daemon-reloadstart + enable
  5. 写 Nginx server 块,软链到 sites-enabled,删默认站点,nginx -treload
  6. 外网访问 IP 或域名,验证 //health
  7. 配 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.logerror.log

适用场景 + 避坑提醒 + 拓展方向

适用场景:单机部署中小流量 FastAPI 服务,没有容器化需求,希望用系统原生工具管理。这套配置在一台 2 核 4G 的机器上跑一个 API 服务绰绰有余。

避坑提醒汇总

  • venv 隔离,别用系统 Python 直接装包。
  • ExecStart 用 venv 里 gunicorn 的绝对路径。
  • /opt/myapp 权限要给 www-data
  • 删掉 Nginx 默认站点。
  • alias 路径结尾带斜杠。
  • worker 数量按实际压测调,别照搬公式。

拓展方向

  1. HTTPS:用 certbot --nginx 自动申请 Let's Encrypt 证书,改 443 监听。这一步我还没在这台机器上做,具体步骤以 certbot 官方文档为准。
  2. 日志切割:Gunicorn 的 access log 会一直涨,可以配 logrotate,或者用 --access-logfile /var/log/myapp/access.log 配合系统的 logrotate。
  3. 数据库连接池:Gunicorn 多 worker 时,每个 worker 是独立进程,数据库连接池要按 worker 数配置,否则总连接数会超。这一点我没在本文展开。
  4. 容器化:如果以后要上多机,可以考虑 Docker,但那是另一个话题。

有问题评论区留言,看到都会回。

相关推荐
IT_陈寒1 小时前
Java空指针这次真把我坑惨了
前端·人工智能·后端
hanchenxing1 小时前
AI 数据管道重构:从函数堆到链式管道的可读性实践
python
hhzz1 小时前
【OpenCV 入门到精通 10】视频分析与光流跟踪:背景减除与运动检测
人工智能·python·opencv·性能优化
蓝胖的四次元口袋1 小时前
Python基础语法入门
python
昇腾知识体系1 小时前
K8s 调度昇腾 NPU:device-plugin 部署、Volcano 与 vNPU 切分
人工智能·华为·知识图谱
海带紫菜菠萝汤1 小时前
本周 AI 观察:嘴上喊着降速,手上全踩油门
人工智能·深度学习·ai·开源·大模型
东风破_1 小时前
把 Elasticsearch 全文检索讲明白:倒排索引、IK 分词器和 BM25
人工智能
远翔调光芯片^138287988721 小时前
ECP5702能芯科技PD取电芯片在市场上的优势有哪些?
开发语言·人工智能·单片机·嵌入式硬件·智能家居
东风破_1 小时前
从 RAG 到 Agentic RAG:第三步,本地知识不够就去网络搜索
人工智能