部署实战:Nginx + Gunicorn 把 Flask+Vue 应用搬上公网
10. 部署实战:Nginx + Gunicorn 把 Flask+Vue 应用搬上公网
前两篇我们分别跑通了「留言板」与「论坛」两个项目,但都是在本机
python app.py/flask run直接启动。这种方式只能用于开发:单进程、无守护、崩溃即停、并发能力近乎为零。本文带你把这两个项目以生产级姿态搬上华为云 ECS,用Gunicorn 做应用服务器 + Systemd 做进程守护 + Nginx 做反向代理,并实现开机自启、公网访问、平滑排错。
10.1 为什么不能直接flask run上生产
Flask 自带的 Werkzeug 开发服务器,官方文档明确写着:
Do not use the development server in production.
原因有四点:
| 维度 | 开发服务器 (flask run) | 生产服务器 (Gunicorn) |
|---|---|---|
| 并发 | 单线程,串行处理请求 | 多 worker 进程,真正并行 |
| 稳定性 | 进程挂了整个服务没了 | worker 异常会被 master 拉起 |
| 性能 | 无优化,慢 | 预 fork、长连接、keep-alive |
| 安全 | 暴露调试器(危险) | 关闭调试,隔离环境 |
Vue 的前端(index.html+app.js+style.css)本质是静态文件,本不该由 Python 去伺候,交给 Nginx 这类专业 Web 服务器效率最高。于是标准生产架构是:
浏览器 ──HTTP──> Nginx (:80) │ 静态资源直接返回(js/css/img) │ 动态请求反向代理 ▼ Gunicorn (:5000 / :8000) │ ▼ Flask 应用(多 worker 进程)10.2 服务器准备(Ubuntu 24.04 踩坑)
在两台华为云 ECS(Ubuntu 24.04)上,第一步是建立隔离的 Python 运行环境。这里有个大坑:Ubuntu 24.04 默认没有venv模块,python3 -m venv会静默失败或报ensurepip缺失。必须先装包:
sudoapt-getupdatesudoapt-getinstall-ypython3-pip python3-venv nginxgitcurl# 关键:没有下面这行,venv 创建会失败sudoapt-getinstall-ypython3.12-venv创建专用虚拟环境(我们用/opt/appenv,与代码分离,方便多项目复用):
sudopython3-mvenv /opt/appenv /opt/appenv/bin/pipinstall--upgradepip /opt/appenv/bin/pipinstallflask flask-sqlalchemy flask-cors flask-wtf gunicorn经验:把依赖装进
/opt/appenv而不是系统 Python,避免污染系统、也避免权限问题。多台机器用同一条命令即可批量准备——我们正是用脚本对 4 台 ECS并行完成的。
10.3 Gunicorn:把 Flask 跑成多进程服务
Gunicorn(Green Unicorn)是 WSGI HTTP 服务器。对于我们的两个项目:
留言板(入口app.py里的app对象):
/opt/appenv/bin/gunicorn-w4-b127.0.0.1:5000 app:app论坛(工厂模式,入口run.py的app对象):
/opt/appenv/bin/gunicorn-w4-b127.0.0.1:8000 run:app参数含义:
-w 4:开 4 个 worker 进程。经验公式(2 × CPU核数) + 1;本机 8 vCPU,取 4~9 皆可。-b 127.0.0.1:5000:只监听本机回环地址,不直接暴露公网——公网流量统一由 Nginx 反代进来,更安全。app:app/run:app:格式为模块:对象,即「从 app.py / run.py 里导入名为 app 的 Flask 实例」。
⚠️致命坑:Gunicorn 启动时不会执行
if __name__ == '__main__':里的db.create_all()(那是给python app.py用的)。所以数据库表必须显式建一次:cd/opt/message-board /opt/appenv/bin/python-c"import app; app.db.create_all()"论坛同理(
from app import db后在app.app_context()里建表)。否则服务起来了,一访问接口就报no such table。
10.4 Systemd:让服务崩溃自启、开机自启
手动跑 Gunicorn 一旦关掉 SSH 就停了。用 Systemd 托管才能持久。
/etc/systemd/system/message-board.service:
[Unit] Description=Message Board (Flask + Vue) After=network.target [Service] User=root WorkingDirectory=/opt/message-board ExecStart=/opt/appenv/bin/gunicorn -w 4 -b 127.0.0.1:5000 app:app Restart=always RestartSec=3 [Install] WantedBy=multi-user.target论坛类似,改路径、端口、入口即可:
[Unit] Description=Forum SPA (Flask + Vue) After=network.target [Service] User=root WorkingDirectory=/opt/forum ExecStart=/opt/appenv/bin/gunicorn -w 4 -b 127.0.0.1:8000 run:app Restart=always RestartSec=3 [Install] WantedBy=multi-user.target启用并启动:
sudosystemctl daemon-reloadsudosystemctlenablemessage-board forum# 开机自启sudosystemctl start message-board forumsudosystemctl status message-board forum# 确认 active (running)Restart=always意味着进程异常退出会被立刻拉起,配合RestartSec=3避免疯狂重启。
10.5 Nginx:反向代理 + 静态资源
/etc/nginx/sites-available/message-board(留言板):
server { listen 80; server_name 120.46.128.206; # 也可填域名 # 静态前端 location / { root /opt/message-board/static; try_files $uri $uri/ /index.html; } # 动态接口反代到 Gunicorn location /api/ { proxy_pass http://127.0.0.1:5000; 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; } }论坛的 Nginx 多了一层Vue history 模式处理(见 10.6):
server { listen 80; server_name 124.71.226.254; location / { root /opt/forum/static; try_files $uri $uri/ /index.html; # SPA 兜底,刷新不 404 } location /api/ { 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; } }启用站点并校验:
sudoln-s/etc/nginx/sites-available/message-board /etc/nginx/sites-enabled/sudoln-s/etc/nginx/sites-available/forum /etc/nginx/sites-enabled/sudonginx-t# 语法检查sudosystemctl reload nginx注意:华为云安全组要放行入方向 80 端口(TCP),否则外网访问不通。我们两个应用都已验证公网 200,说明安全组已正确放行。
10.6 解决 Vue history 模式「刷新 404」
Vue-Router 用history模式时,URL 是/posts/123这种干净路径。但用户直接访问或刷新/posts/123时,Nginx 找不到这个物理文件,会返回 404。
解决有两层:
前端层:Nginx 的try_files $uri $uri/ /index.html;已经把所有未命中文件名的请求兜底到index.html,由 Vue-Router 在前端再路由。这是 SPA 标准做法。
后端层(双保险):Flask 也要能接住任意前端路由,返回index.html:
# app/__init__.py 或 run.py 中@app.route('/',defaults={'path':''})@app.route('/<path:path>')defcatch_all(path):# 只兜底前端路由,API 交给蓝图ifpath.startswith('api/'):returnabort(404)returnapp.send_static_file('index.html')静态文件由 Nginx 直接返回,只有/api/才进 Flask,职责清晰、性能最好。
10.7 CORS 与跨域
开发时前端npm run dev跑在localhost:8080,后端在:5000,浏览器会因同源策略拦截。我们后端统一开了 CORS:
fromflask_corsimportCORS CORS(app,resources={r"/api/*":{"origins":"*"}})生产上前端和后端同域同端口(都走 Nginx 的 80),其实已不存在跨域,CORS 可以收紧为具体域名。但保留*对调试友好,正式上线建议改成白名单:
CORS(app,origins=["http://124.71.226.254","https://your.domain"])10.8 健康检查与排错清单
部署后按这个顺序自查:
# 1) Gunicorn 进程在不在systemctl status message-board# 2) 端口在不在监听ss-tlnp|grep-E'5000|8000'# 3) 本地接口通不通(绕过 Nginx)curl-s-o/dev/null-w"%{http_code}\n"http://127.0.0.1:5000/api/messages# 4) Nginx 通不通curl-s-o/dev/null-w"%{http_code}\n"http://127.0.0.1/# 5) 公网通不通(从另一台机器或本地)curl-s-o/dev/null-w"%{http_code}\n"http://120.46.128.206/常见故障:
| 现象 | 原因 | 解决 |
|---|---|---|
接口 500,no such table | 没建表 | python -c "import app; app.db.create_all()" |
| 刷新子路由 404 | history 模式未兜底 | Nginxtry_files+ 后端 catch-all |
| 502 Bad Gateway | Gunicorn 没起/端口错 | systemctl status看日志 |
| 公网连不上 | 安全组未放行 80 | 云控制台加入方向 80 |
| 静态 JS 404 | root路径写错 | 确认指向static目录 |
日志位置:
journalctl-umessage-board-n50--no-pager# 应用日志tail-f/var/log/nginx/error.log# Nginx 错误10.9 一键部署脚本(复用思路)
我们把上述步骤写成mb_setup.sh/fm_setup.sh,核心流程:
mkdir -p /opt/message-board/static- 建表(显式
db.create_all()) - 写 Systemd unit 到
/etc/systemd/system/ - 写 Nginx site 并
ln -s启用 systemctl daemon-reload && enable && start、nginx -t && reload- 冒烟测试
curl
这样同一份脚本在 4 台 ECS 上并行跑,几分钟就完成两套环境的交付。这也正是本次训练营「5 台服务器高效并行」的落地方式。
10.10 小结与下一步
至此,两个项目都已以生产姿态上线:
- 留言板:http://120.46.128.206
- 论坛:http://124.71.226.254(测试账号
testuser / 123456)
它们都跑在Gunicorn + Systemd + Nginx之上,开机自启、崩溃自拉、公网可达。
进阶( Production hardening,建议):
- HTTPS:用 Let’s Encrypt 免费证书,
certbot --nginx一行搞定,把listen 80升级为 443。 - 静态资源上 CDN / 对象存储,减轻 ECS 压力。
- 数据库换 PostgreSQL,敏感配置走环境变量而非硬编码。
- Markdown 渲染务必加
bleach做 XSS 净化(论坛帖子正文就是典型风险点)。
下一篇(11)我们讲如何把源码与博客发布到 Gitee,让成果可分享、可协作。
