这是整门课最「出成果」的一个阶段——你要把前五个阶段的成果,变成一个「别人在手机上打开就能用」的公网网址。本地能跑只证明代码正确;上线要额外解决四件事:后端不能靠一个终端窗口活着(进程守护)、静态文件要有人高效地发(反向代理)、前后端要共用一个域名(Nginx 一肩挑)、数据库要连得上且不对外暴露。本课程主推 Nginx + Gunicorn,同时给出 Apache 对照方案,做到「教材能跑通、主流也不缺」。
端口 | 协议 | 来源 | 用途 | 是否必须
22 | TCP | 你的公网 IP | SSH 远程登录 | 必须(最好限定来源 IP)
80 | TCP | 0.0.0.0/0 | HTTP,网站入口 | 必须
443 | TCP | 0.0.0.0/0 | HTTPS,网站入口 | 配了证书才需要
---- 以下端口绝对不要对外放行 ----
5000 | TCP | —— | Gunicorn(只监听 127.0.0.1)| 不放行
3306 | TCP | —— | MySQL(只允许本机连接) | 不放行
# ① 更新系统软件包
sudo apt update && sudo apt upgrade -y
# ② 时区设成东八区(否则日志时间会差 8 小时,排查问题时会看错)
sudo timedatectl set-timezone Asia/Shanghai
# ③ 建一个跑应用的普通用户 www(不要让应用以 root 身份运行)
sudo useradd -m -s /bin/bash www
# ④ 建项目根目录并交给 www 用户
sudo mkdir -p /www/hiwen
sudo chown -R www:www /www/hiwen
# ⑤ 本机防火墙(如果云平台已有安全组,ufw 可以不开;开了就必须放行 SSH)
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
# ① 系统自带 Python 3.10(Ubuntu 22.04),先装 pip 与虚拟环境工具
sudo apt install -y python3-pip python3-venv python3-dev build-essential
# ② 进项目目录,建虚拟环境(会生成 venv/ 目录)
cd /www/hiwen
python3 -m venv venv
# ③ 激活并把后端依赖装进去(gunicorn 也一起装)
source venv/bin/activate
pip install --upgrade pip
pip install -r hiflask/requirements.txt
pip install gunicorn
# ① 安装 MySQL 8
sudo apt install -y mysql-server
sudo systemctl enable --now mysql
# ② 从本机把 hiwen.sql 上传到服务器(在你自己电脑的终端执行)
# scp hiwen.sql root@你的公网IP:/www/hiwen/
# ③ 建库并导入(在服务器上执行)
sudo mysql -e "CREATE DATABASE hiwen DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mysql hiwen < /www/hiwen/hiwen.sql
# ④ 建应用专用账号:只允许本机登录,且只给 hiwen 库的权限
sudo mysql -e "CREATE USER 'hiwen'@'localhost' IDENTIFIED BY '换成强密码';"
sudo mysql -e "GRANT ALL PRIVILEGES ON hiwen.* TO 'hiwen'@'localhost'; FLUSH PRIVILEGES;"
# ⑤ 确认数据真的进来了
sudo mysql hiwen -e "SELECT COUNT(*) AS 作品数 FROM article;"
# ① 装 Node 20 LTS(NodeSource 源)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# ② 上传前端源码(在你自己电脑的终端执行,排除 node_modules)
# rsync -av --exclude node_modules hivue/ root@你的公网IP:/www/hiwen/hivue/
# ③ 在服务器上装依赖并构建
cd /www/hiwen/hivue
npm install
npm run build
# ④ 把打包产物移动到 Nginx 要读的目录
mv dist /www/hiwen/dist
sudo chown -R www:www /www/hiwen
import os
# ── 给 Gunicorn / mod_wsgi 用的模块级实例 ──
# 生产环境读 prod 配置(DEBUG=False,密钥来自环境变量)
# 启动命令:gunicorn -w 4 -b 127.0.0.1:5000 app:app
app = create_app(os.getenv("FLASK_CONFIG", "prod"))
if __name__ == "__main__":
# 本地开发仍然走 dev 配置(有调试器和热重载)
create_app("dev").run(host="127.0.0.1", port=5000, debug=True)
# 生产环境配置(此文件绝不能提交到 Git!)
FLASK_CONFIG=prod
# 数据库连接(用上一步建的应用账号,不是 root)
DB_USER=hiwen
DB_PASSWORD=换成和建账号时一致的强密码
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=hiwen
# 密钥:用 python -c "import secrets; print(secrets.token_hex(32))" 生成
SECRET_KEY=替换成随机生成的 64 位十六进制串
JWT_SECRET_KEY=替换成另一个随机生成的 64 位十六进制串
JWT_EXPIRES_DAYS=7
# 上传目录(生产环境建议放到代码目录之外,便于备份与权限管理)
UPLOAD_FOLDER=/www/hiwen/uploads
# 切到 www 用户,进后端目录,激活虚拟环境
sudo -u www -i
cd /www/hiwen/hiflask
source /www/hiwen/venv/bin/activate
# -w 4:开 4 个 worker 进程(经验值:CPU 核数 × 2 + 1)
# -b 127.0.0.1:5000:只监听本机回环地址,外网访问不到(安全!)
gunicorn -w 4 -b 127.0.0.1:5000 app:app
[Unit]
Description=古典文学网后端(Gunicorn)
# 等网络就绪再启动,避免启动时数据库还没准备好
After=network.target mysql.service
Wants=mysql.service
[Service]
# 用普通用户运行(绝不要用 root)
User=www
Group=www
# 工作目录:gunicorn 在这里找 app.py
WorkingDirectory=/www/hiwen/hiflask
# 环境变量文件:数据库密码、密钥都放在这里面
EnvironmentFile=/www/hiwen/hiflask/.env
# 也可以在这里直接补单个变量
Environment=FLASK_CONFIG=prod
# 启动命令:绝对路径调用 venv 里的 gunicorn(systemd 不读 PATH)
# -w 4 → 4 个 worker
# -b 127.0.0.1:5000 → 只监听本机
# --access-logfile - → 访问日志打到标准输出,由 journal 收集
ExecStart=/www/hiwen/venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 \
--timeout 60 --access-logfile - --error-logfile - app:app
# 崩溃后自动重启(always:不管什么原因退出都拉起来)
Restart=always
RestartSec=3
# 优雅停止:先发 SIGTERM,5 秒后还在就 SIGKILL
KillSignal=SIGTERM
TimeoutStopSec=5
# 安全加固(可选但推荐)
NoNewPrivileges=true
PrivateTmp=true
[Install]
# 设为 enabled,开机自启
WantedBy=multi-user.target
# ① 启动 / 停止 / 重启
sudo systemctl start hiwen
sudo systemctl stop hiwen
sudo systemctl restart hiwen # 改完后端代码,用它生效
# ② 看状态(active (running) 才是正常的)
sudo systemctl status hiwen --no-pager
# ③ 看实时日志(-f 跟随输出,Ctrl+C 退出)
# 排查 500 错误、看请求日志全靠它
sudo journalctl -u hiwen -f
# ④ 只看最近 100 行
sudo journalctl -u hiwen -n 100 --no-pager
sudo apt install -y nginx
sudo systemctl enable --now nginx
# 默认站点会占着 80 端口,把它停用掉,换成我们自己的配置
sudo rm -f /etc/nginx/sites-enabled/default
server {
# 监听 80 端口,先写 IP 或域名都行
listen 80;
server_name example.com www.example.com;
# 前端打包产物的根目录(Nginx 从这里发 index.html 和 assets)
root /www/hiwen/dist;
index index.html;
# 上传图片最大 4M:头像限制 2M,但 multipart 有额外开销,留些余量
# 注意:Nginx 默认只有 1m,不改的话上传 2M 头像会返回 413
client_max_body_size 4m;
# 日志:排障时先看这里
access_log /var/log/nginx/hiwen.access.log;
error_log /var/log/nginx/hiwen.error.log;
# ── ① 前端静态资源 + history 路由兜底 ──────────────
# 先找真实文件(/assets/xxx.js、/favicon.ico),找不到就交给 index.html,
# 由 Vue Router 在前端决定渲染哪个页面。没有这一行,刷新 /article/1 会 404。
location / {
try_files $uri $uri/ /index.html;
}
# 带哈希的打包产物可以长期缓存(文件名一变就是新文件)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# ── ② 后端接口反向代理到 Gunicorn ───────────────────
location /api/ {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
# 把真实信息透传给 Flask,否则后端只能看到 127.0.0.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;
# 鉴权头必须显式透传,否则后端拿不到 Authorization,登录接口全返回 401
proxy_set_header Authorization $http_authorization;
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
}
# ── ③ 上传目录与后端静态资源 ───────────────────────
# 后端返回的头像路径形如 /static/upload/avatar/3.png,直接由 Nginx 发文件,
# 比让 Flask 一个一个读文件快得多。
location /static/ {
alias /www/hiwen/hiflask/static/;
expires 7d;
add_header Cache-Control "public";
}
# 如果按生产最佳实践把上传目录挪到了代码之外(UPLOAD_FOLDER=/www/hiwen/uploads),
# 就再加这一段,让 /uploads/ 指向它。
location /uploads/ {
alias /www/hiwen/uploads/;
expires 7d;
add_header Cache-Control "public";
}
# ── ④ 隐藏文件与配置文件不让访问 ───────────────────
location ~ /\.(?!well-known) {
deny all;
}
}
<VirtualHost *:80>
ServerName example.com
# 前端打包产物目录
DocumentRoot /www/hiwen/dist
<Directory /www/hiwen/dist>
Require all granted
# 关键:SPA 的 history 路由兜底,等价于 Nginx 的 try_files
FallbackResource /index.html
</Directory>
# 加载代理模块(首次需要)
# sudo a2enmod proxy proxy_http
# ① 接口反向代理到 Gunicorn,等价于 Nginx 的 proxy_pass
ProxyPass /api/ http://127.0.0.1:5000/api/
ProxyPassReverse /api/ http://127.0.0.1:5000/api/
# 把 Authorization 头透传给后端,否则鉴权接口全部 401
ProxyPassReverse /api/ http://127.0.0.1:5000/api/
# ② 后端静态资源与上传文件(等价于 Nginx 的 location /static/)
Alias /static/ /www/hiwen/hiflask/static/
<Directory /www/hiwen/hiflask/static>
Require all granted
</Directory>
# ③ 上传体积上限(等价于 client_max_body_size 4m)
LimitRequestBody 4194304
ErrorLog /var/log/apache2/hiwen.error.log
CustomLog /var/log/apache2/hiwen.access.log combined
</VirtualHost>
<VirtualHost *:80>
ServerName example.com
# mod_wsgi:把整个 Flask 应用挂到根路径
# python-home 指虚拟环境,python-path 指代码目录
WSGIDaemonProcess hiwen python-path=/www/hiwen/hiflask python-home=/www/hiwen/venv
WSGIProcessGroup hiwen
WSGIScriptAlias / /www/hiwen/hiflask/wsgi.py
# 允许 Apache 读取入口文件
<Directory /www/hiwen/hiflask>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
# 让 /static/ 由 Apache 直出(比让 Flask 发文件快)
Alias /static/ /www/hiwen/hiflask/static/
<Directory /www/hiwen/hiflask/static>
Require all granted
</Directory>
LimitRequestBody 4194304
</VirtualHost>
# 配套的入口文件 /www/hiwen/hiflask/wsgi.py:
# from app import create_app
# application = create_app("prod")
# 安装:sudo apt install -y libapache2-mod-wsgi-py3
# 注意 —— mod_wsgi 必须装进和虚拟环境同一个 Python 版本的 Apache 模块,
# 混用不同 Python 版本会出现 「ModuleNotFoundError: No module named 'flask'」
# ① 确认域名解析已生效(应该返回你的公网 IP)
dig +short example.com
# ② 安装 certbot 与 Nginx 插件
sudo apt install -y certbot python3-certbot-nginx
# ③ 一条命令:申请证书 + 自动改写 Nginx 配置 + 配置 80 跳转 443
sudo certbot --nginx -d example.com -d www.example.com
# ④ 验证自动续期(certbot 会装一个 systemd 定时器,每天检查两次)
sudo certbot renew --dry-run
[ ] 首页可开 curl -I http://example.com → 200 OK
[ ] 接口可通 curl http://example.com/api/category/list → {"code":200,...}
[ ] 刷新不 404 curl -I http://example.com/article/1 → 200 OK(不是 404)
[ ] 静态资源可加载 curl -I http://example.com/assets/index-xxx.js → 200 OK
[ ] 上传可用 在页面里上传一张头像 → 返回 {"avatar":"/static/upload/..."}
[ ] 图片可显示 curl -I http://example.com/static/upload/avatar/x.png → 200 OK
[ ] 登录可用 页面登录成功、顶栏显示昵称、刷新不掉登录态
[ ] 服务自启 sudo reboot 后重新访问 → 无需手动启动就能用
[ ] 后端只监听本机 ss -lntp | grep 5000 → 127.0.0.1:5000
[ ] 服务无报错 sudo journalctl -u hiwen -n 50 → 无 ERROR
症状 病因 药方
502 Bad Gateway Gunicorn 没起来 / 崩了 / 端口不对 systemctl status hiwen;journalctl -u hiwen -n 50
(Nginx 连不上后端) 确认 ss -lntp | grep 5000 有监听
403 Forbidden Nginx 没有读目录权限 / 目录里没文件 chown -R www:www /www/hiwen/dist;ls 看文件在不在
(找不到 index 且没开 autoindex) 检查 root 路径有没有写错
404(刷新详情页才 404) try_files 没配,SPA 路由没兜底 加 try_files $uri $uri/ /index.html
404(图片打不开) alias/root 用错,或结尾少了斜杠 location /static/ { alias /.../static/; } 两边都要 /
413 Request Entity Too Large client_max_body_size 默认只有 1m 加 client_max_body_size 4m; 并 reload
401(本地正常、线上全 401) Nginx 没透传 Authorization 头 加 proxy_set_header Authorization $http_authorization;
500(页面能开、接口报错) Flask 抛异常 / 数据库连不上 journalctl -u hiwen 看堆栈;核对 .env 里的库账号密码
status=203/EXEC ExecStart 写了裸命令名 改成绝对路径 /www/hiwen/venv/bin/gunicorn
status=200/CHDIR WorkingDirectory 路径不存在 核对路径拼写与实际目录
Connection refused(curl) 服务没通过安全组 / ufw 没放行 80 云控制台加规则 + sudo ufw allow 80/tcp
ModuleNotFoundError systemd 用了系统 Python 而非 venv ExecStart 用 venv 里的绝对路径可执行文件
# 部署完成后的「四件套」,你交的说明文档里应该能一一对应
[ ] /etc/systemd/system/hiwen.service —— 进程守护(WorkingDirectory / EnvironmentFile / ExecStart / Restart)
[ ] /www/hiwen/hiflask/.env —— 生产密钥与数据库连接(权限 600,不入 Git)
[ ] /etc/nginx/sites-available/hiwen.conf —— 静态托管 + /api 反代 + /static 上传 + try_files
[ ] /www/hiwen/dist/index.html —— 前端打包产物,Nginx 的 root 指向它
# 验证「Gunicorn 只监听本机」的两种方式
$ ss -lntp | grep 5000
LISTEN 0 2048 127.0.0.1:5000 0.0.0.0:* users:(("gunicorn",pid=2530,...))
# 从外网扫这个端口应该是「连接被拒绝」—— 说明它没有暴露
$ nc -zv example.com 5000
nc: connect to example.com port 5000 (tcp) failed: Connection refused
【现象】访问 http://example.com/api/category/list 返回 502 Bad Gateway
【第 1 层:Gunicorn 活着吗?】
$ sudo systemctl status hiwen --no-pager
Active: failed (Result: exit-code)
Process: ExecStart=/www/hiwen/venv/bin/gunicorn ... (code=exited, status=1/FAILURE)
→ 后端根本没起来,「502 怪后端」得到印证
【第 2 层:为什么起不来?看日志】
$ sudo journalctl -u hiwen -n 8 --no-pager
ModuleNotFoundError: No module named 'app'
→ 找不到 app 模块
【第 3 层:gunicorn 在哪找 app?】
gunicorn 是在「当前工作目录」里找 app.py 的,而工作目录来自
systemd 的 WorkingDirectory。去核对一眼:
$ grep WorkingDirectory /etc/systemd/system/hiwen.service
WorkingDirectory=/www/hiwen ← 错了!代码在 /www/hiwen/hiflask
【修复与验证】
$ sudo sed -i 's#WorkingDirectory=/www/hiwen$#WorkingDirectory=/www/hiwen/hiflask#' \
/etc/systemd/system/hiwen.service
$ sudo systemctl daemon-reload
$ sudo systemctl restart hiwen
$ sudo systemctl status hiwen --no-pager | grep Active
Active: active (running)
$ curl -s http://127.0.0.1:5000/api/category/list
{"code":200,"msg":"success","data":[...]} ← 恢复
# /etc/nginx/sites-available/movie.conf(只列需要改动的部分)
server {
listen 80;
server_name movie.example.com;
root /www/movie/dist;
index index.html;
# 改动点 ①:上限从 4m 提到 60m(预告片单个最大 50M,留出 multipart 余量)
client_max_body_size 60m;
location / {
try_files $uri $uri/ /index.html;
}
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 Authorization $http_authorization;
# 大文件上传耗时长,读超时要同步放大,否则传到一半被 Nginx 掐断
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_request_buffering off; # 大文件边收边转,别先全缓存到磁盘
}
# 改动点 ②:新增视频目录,由 Nginx 直出(视频文件极大,绝不能走 Flask)
location /uploads/video/ {
alias /www/movie/uploads/video/;
expires 30d;
}
}
第 3 题的核心:文件越大,越要「早拦、少经过 Python」。Nginx 负责在门口挡住超限请求,Flask 负责兜底校验,大文件由 Nginx 直接发送。能说清「为什么两层都要改」,说明你理解了请求在这一整条链路上到底经过了谁。