如何在 VPS 上用 Gunicorn 和 Nginx 运行 Python 应用
在 Ubuntu 上用 virtualenv、Gunicorn systemd 服务、Nginx 反向代理、TLS、环境文件、日志,以及 502 错误和 worker 规模检查清单,部署 Flask 或 FastAPI 应用。

内置 Flask 服务器和 uvicorn --reload 是给开发用的。在公网 VPS 上,你需要能重启 worker、绑定到 localhost、坐在处理 TLS 和慢客户端的反向代理后面的进程管理器。Gunicorn 是 Flask 和 Django 常用的 WSGI 选择。FastAPI 可以在带 Uvicorn worker 类的 Gunicorn 下运行。Nginx(或 Caddy)终止 HTTPS 并转发到 127.0.0.1:8000。
本指南走一遍能撑过重启的布局:项目在 /srv/app,virtualenv,带密钥的 .env,gunicorn.service 单元,Nginx server 块,Let's Encrypt,以及你真正能 grep 的日志文件。我们还会谈 worker 数量、超时,以及为什么 502 Bad Gateway 几乎从来不是「Nginx 坏了」——几乎总是 Gunicorn 没在跑、绑到了错误套接字,或在 import 时崩溃。示例使用 Flask,命令不同处会注明 FastAPI。
为什么是这套栈
Gunicorn 会 prefork worker 进程。除非你用不同的 worker 类,每个 worker 一次处理一个请求。Nginx 缓冲慢客户端,这样 worker 不会卡在往移动网络送字节。systemd 在应用挂掉时重启它。合在一起很无聊,而这正是凌晨三点你想要的。
- Gunicorn:稳定的 WSGI/ASGI 进程模型
- systemd:开机启动、崩溃重启、journald 日志
- Nginx:TLS、静态文件、请求大小限制、gzip
- venv:系统 Python 保持干净
- localhost 绑定:应用只能通过 Nginx 到达
要求
Ubuntu 22.04/24.04 上的 Python 3.10+ 没问题。不要以 root 把 pip 装进系统 site-packages。TLS 需要域名。如果更喜欢 Caddy 当代理,本文的 Gunicorn 单元不变——只改前端配置(见 Caddy 文章)。
- Ubuntu 22.04 或 24.04 VPS
- 带 requirements.txt 或等效文件的应用
- WSGI 入口(Flask:app:app)或 ASGI(FastAPI:带 uvicorn worker 的 app:app)
- HTTPS 用的域名 A 记录
步骤 1:系统软件包、用户和项目目录
创建一个不能交互登录的系统用户,拥有代码并运行 Gunicorn。安装 python3-venv 和构建工具,可避免仍需编译 C 扩展的包在 pip 时失败。
ssh root@YOUR_VPS_IP
apt update && apt -y upgrade
apt -y install python3 python3-venv python3-pip python3-dev build-essential nginx curl
adduser --system --group --home /srv/app appuser
mkdir -p /srv/app
chown appuser:appuser /srv/app
# Copy your code (example):
# rsync -a --delete ./myproject/ appuser@YOUR_VPS_IP:/srv/app/步骤 2:virtualenv 和依赖
以 appuser 创建 venv,这样文件所有权才正确。生产中钉死版本。安装后,确认你能在一次性 gunicorn --check-config 或 Python import 中导入应用。这里的 import 错误,就是后来变成 502 的同一类错误。
sudo -u appuser -H bash -lc '
cd /srv/app
python3 -m venv /srv/app/venv
/srv/app/venv/bin/pip install --upgrade pip
/srv/app/venv/bin/pip install -r /srv/app/requirements.txt gunicorn
'
# Flask example check:
sudo -u appuser -H /srv/app/venv/bin/python -c "from app import app; print('import ok')"步骤 3:环境文件
不要以最终会进全世界可读 git 的方式,把 SECRET_KEY 或数据库 URL 硬编码进 systemd 单元。使用 EnvironmentFile。chmod 640,所有者 root,组 appuser(或如果你更喜欢,所有者 appuser)。
cat >/srv/app/.env <<'EOF'
FLASK_ENV=production
SECRET_KEY=replace-with-openssl-rand-hex-32
DATABASE_URL=postgresql://app:password@127.0.0.1:5432/app
EOF
chown appuser:appuser /srv/app/.env
chmod 600 /srv/app/.env步骤 4:Gunicorn systemd 服务
绑定到 127.0.0.1:8000,而不是 0.0.0.0,除非你有理由跳过 Nginx。sync worker 的数量常常是 (2 x CPU) + 1;在 2 vCPU 的 VPS 上是 5,如果每个 worker 加载很重的 ML 模型可能太多——那就用 2–3。对 FastAPI,设置 --worker-class uvicorn.workers.UvicornWorker 并安装 uvicorn。超时应超过你最慢的诚实请求,如果有 2 分钟的导出就不要用 30 秒。
cat >/etc/systemd/system/gunicorn.service <<'EOF'
[Unit]
Description=Gunicorn for the web app
After=network.target
[Service]
User=appuser
Group=appuser
WorkingDirectory=/srv/app
EnvironmentFile=/srv/app/.env
ExecStart=/srv/app/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 --timeout 60 --access-logfile - --error-logfile - app:app
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now gunicorn
systemctl status gunicorn --no-pager
ss -tulpn | grep 8000步骤 5:Nginx 反向代理和 TLS
Nginx 监听 80/443 并代理到 Gunicorn。上传时 client_max_body_size 很重要。proxy_read_timeout 应匹配或超过 Gunicorn 的超时。server 块在 HTTP 上能工作后,用 Certbot 签发证书(或把前端换成 Caddy)。下面的片段仅 HTTP 以便测试;然后运行可以编辑该文件的 Certbot。
cat >/etc/nginx/sites-available/app <<'EOF'
server {
listen 80;
server_name app.example.com;
client_max_body_size 20m;
location /static/ {
alias /srv/app/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;
proxy_read_timeout 90s;
}
}
EOF
ln -sf /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app
nginx -t && systemctl reload nginx
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable
apt -y install certbot python3-certbot-nginx
certbot --nginx -d app.example.com步骤 6:静态文件、权限以及 Flask/FastAPI 细节
如果可以,应由 Nginx 提供静态文件;这比 Gunicorn 快。Django collectstatic、小应用的 Flask send_from_directory,或以后的 CDN。appuser 必须能读这棵树。如果用 FastAPI,在 venv 中安装 uvicorn[standard],并把 ExecStart 改成使用 UvicornWorker。如果用 Unix 套接字而不是 TCP,把 proxy_pass 指向套接字,并匹配权限以便 www-data 能写入。
# FastAPI ExecStart example:
# ExecStart=/srv/app/venv/bin/gunicorn -k uvicorn.workers.UvicornWorker --workers 2 --bind 127.0.0.1:8000 app:app
# Unix socket variant:
# --bind unix:/run/gunicorn/app.sock
# Nginx: proxy_pass http://unix:/run/gunicorn/app.sock:
chown -R appuser:appuser /srv/appWorker、内存和零停机重载
同步 Gunicorn worker 简单,对 CPU 很轻的请求/响应 API 足够。如果需要大量并发的慢 I/O 等待,考虑 gevent 或 ASGI worker——要测量,不要猜。每个 worker 都会加载你的应用;RAM ~= worker 数乘以应用 RSS。一台 2 GB 的 VPS 上 8 个 300 MB 应用的 worker 会交换,感觉「随机变慢」。如果你就地部署了文件,systemctl reload gunicorn(HUP)可以用新代码重启 worker;依赖变化时完整重启更清楚。
# After git pull / rsync of new code:
sudo -u appuser -H /srv/app/venv/bin/pip install -r /srv/app/requirements.txt
systemctl restart gunicorn
curl -I https://app.example.com/health排查 502 和静默崩溃
502 表示 Nginx 无法从上游得到有效响应。第一个命令是 journalctl -u gunicorn -e。常见原因:错误的 module:app 名、缺少的 .env 键、Postgres 没在跑、绑到 127.0.0.1 但 Nginx 在另一台主机、SELinux(Ubuntu 上很少见),或应用只监听 IPv6。504 是超时。当应用重定向到 HTTP 而 X-Forwarded-Proto 被忽略时,会出现 301 循环。
- systemctl status gunicorn — 是否 active?
- journalctl -u gunicorn -n 100 — ImportError、缺少 env、数据库
- 从 VPS 上 curl -v http://127.0.0.1:8000/ — 如果这失败,Nginx 是无辜的
- nginx -t 和 error.log — upstream prematurely closed connection
- ss -tulpn | grep 8000 — 没有人在听
- 磁盘满 — worker 会以神秘方式崩溃
安全
应用从不公开绑定。密钥留在 .env。保持 venv 和 OS 打补丁。不要以 root 运行 Gunicorn。如果处理登录,设置会话 cookie 的 Secure 和 SameSite,并配置框架只信任来自 Nginx 的 X-Forwarded-Proto。在 Nginx 或应用里对登录路由做速率限制。
- 绑定 127.0.0.1 或 unix 套接字
- chmod 600 .env
- systemd 中 User= 为非 root
- 通过 Certbot 或 Caddy 做 TLS
- 生产中禁用 debug 模式和自动重载
提示
- 加一个检查数据库连通性的 /health 路由,给 Compose 和负载均衡器用
- 在 Nginx 里给静态资源加 cache-control 头
- 邮件和重任务用单独的 worker 或队列(Redis + systemd)
- 在 requirements.txt 中钉死 gunicorn 和 uvicorn 版本
- 第一次生产切换前拍一张快照
当 Python 应用在 Gunicorn 下作为 systemd 服务运行、只绑定 localhost,并通过带 TLS 的 Nginx 或 Caddy 到达时,它在 VPS 上就算生产就绪。把密钥放进 EnvironmentFile,按 RAM 而不是博文公式给 worker 定规模,502 先从 Gunicorn journal 调试。一旦这条路径为你的仓库写好文档,之后每次部署就是 rsync 或 git pull、pip install,以及 systemctl restart gunicorn。