码上问学问代码之理,学编程之道

阶段 6 · 部署上线

这是整门课最「出成果」的一个阶段——你要把前五个阶段的成果,变成一个「别人在手机上打开就能用」的公网网址。本地能跑只证明代码正确;上线要额外解决四件事:后端不能靠一个终端窗口活着(进程守护)、静态文件要有人高效地发(反向代理)、前后端要共用一个域名(Nginx 一肩挑)、数据库要连得上且不对外暴露。本课程主推 Nginx + Gunicorn,同时给出 Apache 对照方案,做到「教材能跑通、主流也不缺」。

部署要解决什么

本地能跑 ≠ 别人能用
部署架构:一个 80 端口,Nginx 兵分两路——静态文件自己扛,/api 转给后端用户浏览器http://服务器IP80 端口Nginx静态 + 反向代理try_files 兜底路由/ → dist 静态文件/api/ → 转发Vue disthtml/css/jsGunicorn多 worker 进程FlaskhiflaskMySQL 8 : 3306开发 vs 生产的两个关键替换· Flask 自带服务器(单线程、调试用)→ Gunicorn(多进程、扛并发)· Vite 代理(开发期跨域)→ Nginx 反向代理(生产期同源)防火墙只开 80/443:MySQL、Gunicorn 的端口绝不对公网开放
先讲为什么你在本机敲 python app.py,浏览器打开 127.0.0.1:5000,一切正常。可这套「本地能跑」的配置,一到线上就处处是问题:
① 你关掉终端 / 断开 SSH,服务就没了——因为它是挂在你这个会话上的;
② Flask 自带的开发服务器单进程、单线程,还带调试器,别说 100 个人,10 个人同时打开就卡;
③ 用户不可能访问 127.0.0.1:5000,他需要一个域名;
④ 前端是 5173 的 Vite 开发服务器,生产环境根本没有它。
部署要做的,就是把「开发环境的一堆临时办法」换成「生产环境的一整套正式配置」。
讲透:一张图记住请求是怎么走完的
  • 用户在浏览器输入 http://example.com,请求打到服务器的 80 端口。
  • Nginx 收下请求,按路径分流:/ 开头的走「前端静态文件」;/api/ 开头的反向代理给本机的 Gunicorn。
  • Gunicorn 在 127.0.0.1:5000 上跑着 4 个 worker,把请求交给 Flask 应用处理。
  • Flask 按蓝图找到对应视图,查 MySQL 8,把 JSON 结果原路返回。
  • 关键点:Gunicorn 只听 127.0.0.1,外网碰不到它;MySQL 同理,只有本机应用能连。所有对外入口只有 Nginx 的 80/443——这就是「收敛攻击面」。

服务器准备

选型 / 安全组 / 系统更新
先讲为什么「安全组」这个词听起来玄乎,其实它就是云平台在服务器外面加的一道防火墙:你不在里面放行某个端口,外面的请求根本到不了服务器。最常见的「部署完了打不开」就是安全组没放行 80 端口——代码、配置全对,就是进不来。
动手做买一台云服务器(阿里云 / 腾讯云 / 华为云的轻量应用服务器即可),配置:2 核 2G、系统盘 40G、Ubuntu 22.04 LTS。然后到控制台的「安全组 / 防火墙」里放行三个端口。
安全组端口规则text
端口   | 协议 | 来源        | 用途                     | 是否必须
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(只允许本机连接)  | 不放行
效果预览运行结果
运行结果: # 安全组配好后,先确认服务器能连上 $ ssh root@你的公网IP Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-119-generic x86_64) # 记下你的公网 IP,后面配 Nginx 和域名解析都要用 $ curl -s ifconfig.me 123.45.67.89
动手做更新系统、设时区、建一个专门跑应用的普通用户(别用 root 跑服务),并把项目目录的属主给他。
服务器初始化bash
# ① 更新系统软件包
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
终端输出命令行执行结果
运行结果: $ timedatectl Local time: Tue 2026-10-15 10:05:21 CST Universal time: Tue 2026-10-15 02:05:21 UTC Time zone: Asia/Shanghai (CST, +0800) $ sudo ufw status Status: active To Action From -- ------ ---- 22/tcp ALLOW Anywhere 80/tcp ALLOW Anywhere 443/tcp ALLOW Anywhere
避坑两件事千万别做:① 不要把 5000 或 3306 端口对外放行。图省事开一个 5000,等于把「没有 Nginx 保护、没有限流、带完整调试信息的应用」直接暴露在公网,扫描器几分钟就能找到你;3306 更是重灾区,数据库被拖库多半就是这个口开的。② 不要用 root 跑应用。一旦应用被攻破,攻击者拿到的就是服务器最高权限。用普通用户 www 跑,出事的爆炸半径小得多。

服务器装环境

Python / 虚拟环境 / MySQL 8 / Node
动手做装 Python 与虚拟环境工具。生产环境必须用虚拟环境:项目的依赖和系统 Python 彻底隔离,免得装了 A 项目把 B 项目搞崩。
安装 Python 环境bash
# ① 系统自带 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
终端输出命令行执行结果
运行结果: $ python3 --version Python 3.10.12 $ pip list | grep -iE "flask|gunicorn|pymysql" Flask 3.0.3 Flask-SQLAlchemy 3.1.1 Flask-JWT-Extended 4.6.0 Flask-CORS 4.0.1 PyMySQL 1.1.1 gunicorn 22.0.0 # 注意:venv 是绝对独立的,退出后系统 Python 里不会有这些包 $ deactivate
动手做装 MySQL 8、建库、导入阶段 2 的 hiwen.sql。关键:建一个只能本机访问的应用账号,别让应用用 root 连库。
安装 MySQL 8 并建库bash
# ① 安装 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;"
终端输出命令行执行结果
运行结果: $ sudo systemctl status mysql --no-pager | head -3 ● mysql.service - MySQL Community Server Loaded: loaded (/lib/systemd/system/mysql.service; enabled; preset: enabled) Active: active (running) since Tue 2026-10-15 10:12:08 CST; 1min ago $ sudo mysql hiwen -e "SELECT id,title,author FROM article LIMIT 3;" +----+------------------+--------+ | id | title | author | +----+------------------+--------+ | 1 | 静夜思 | 李白 | | 2 | 春望 | 杜甫 | | 3 | 念奴娇·赤壁怀古 | 苏轼 | +----+------------------+--------+ -- 导入成功
动手做装 Node,把本地前端代码上传到服务器,构建出 dist/。前端只在服务器上构建一次,构建完就不需要 Node 了——Nginx 直接发静态文件。
构建前端bash
# ① 装 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
终端输出命令行执行结果
运行结果: $ node -v v20.18.0 $ npm run build vite v5.4.8 building for production... ✓ 62 modules transformed. dist/index.html 0.48 kB │ gzip: 0.32 kB dist/assets/index-Dx3k1a2b.css 38.21 kB │ gzip: 7.94 kB dist/assets/index-C9mQp4tR.js 196.44 kB │ gzip: 68.10 kB ✓ built in 2.71s $ ls /www/hiwen/dist assets index.html favicon.ico
避坑两个关键点:① 生产环境一定要用 prod 配置(DEBUG=False)。Flask 在 debug 模式下会把 Werkzeug 调试器暴露出来,任何人可以在网页上直接执行任意 Python 代码,这是最严重的安全漏洞之一。② 数据库密码写成环境变量,别写进代码。项目里 config.py 是从环境变量读的,所以你要在服务器上放一个 .env 文件,并且绝不能把它提交到 Git 仓库。

后端部署:Gunicorn

把开发服务器换成生产级服务器
先讲为什么Flask 自带的 app.run() 是「开发用服务器」——官方文档明确说不适合生产:它单进程处理请求,一个慢请求会把后面的全堵住;还带自动重载和调试器。生产环境需要一个真正的 WSGI 服务器:Gunicorn 会预先 fork 出多个 worker 进程,请求来了由空闲的 worker 处理,一个卡住不影响其他。你要做的只是给它一个入口,然后决定开几个 worker。
动手做Gunicorn 需要一个「模块级」的 app 变量。我们的 app.py 用的是应用工厂(只有 create_app()),所以在文件末尾补一行,让它能通过 app:app 被加载。
hiflask/app.py(末尾追加)python
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)
运行输出Python 运行结果
运行结果: # 确认模块级的 app 能被正确加载(不启动服务器,只导入) $ cd /www/hiwen/hiflask $ FLASK_CONFIG=prod /www/hiwen/venv/bin/python -c "import app; print(app.app)" <Flask 'app'> # 如果这里报错,说明 .env 里的数据库配置有问题 —— 先修好再往下走
动手做写 .env(生产密钥与数据库连接),然后用 Gunicorn 把服务跑起来看看。先手动跑一次,确认能起来,再去配 systemd——这样出了问题好定位。
/www/hiwen/hiflask/.envini
# 生产环境配置(此文件绝不能提交到 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
效果预览运行结果
运行结果: # 生成两个强密钥(各跑一次,把输出填进 .env) $ python3 -c "import secrets; print(secrets.token_hex(32))" 9f3c1a7e5b2d48c6a0f1e7b3d5942860c7a1b8e4f2d6c3095a7b1e4d8f2c6a35 # 给 .env 收紧权限:只允许 www 用户读(数据库密码就在里面) $ chown www:www /www/hiwen/hiflask/.env $ chmod 600 /www/hiwen/hiflask/.env $ ls -l /www/hiwen/hiflask/.env -rw------- 1 www www 412 Oct 15 10:20 /www/hiwen/hiflask/.env
手动启动 Gunicorn(验证用)bash
# 切到 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
终端输出命令行执行结果
运行结果: [2026-10-15 10:25:03 +0800] [2411] [INFO] Starting gunicorn 22.0.0 [2026-10-15 10:25:03 +0800] [2411] [INFO] Listening at: http://127.0.0.1:5000 (2411) [2026-10-15 10:25:03 +0800] [2411] [INFO] Using worker: sync [2026-10-15 10:25:03 +0800] [2414] [INFO] Booting worker with pid: 2414 [2026-10-15 10:25:03 +0800] [2415] [INFO] Booting worker with pid: 2415 [2026-10-15 10:25:03 +0800] [2416] [INFO] Booting worker with pid: 2416 [2026-10-15 10:25:03 +0800] [2417] [INFO] Booting worker with pid: 2417 # 另开一个终端验证(注意:不加 &&,两条命令分开跑) $ curl -s "http://127.0.0.1:5000/api/article/recommend?limit=1" {"code":200,"msg":"success","data":[{"id":3,"title":"念奴娇·赤壁怀古","author":"苏轼", "dynasty":"宋","category_name":"宋词","views":412,"likes":58}]} # 验证完按 Ctrl+C 停掉,接下来交给 systemd 托管
讲透:worker 数量与绑定地址
  • -w 开几个?常用经验公式是 CPU 核数 × 2 + 1。2 核机器开 4–5 个;开太多反而会因为进程切换而变慢。真实数量要靠压测定,别迷信公式。
  • 为什么绑 127.0.0.1 不绑 0.0.0.0?绑回环地址意味着「只有本机的进程能连上它」,外部完全访问不到。Nginx 和 Gunicorn 在同一台机器上,走回环即可——这样 Gunicorn 就天然不需要被保护。
  • 超时与日志:生产里通常还会加 --timeout 60 --access-logfile - --error-logfile -,把访问日志交给 systemd/journal 统一收集。
  • Gunicorn 是「应用服务器」,Nginx 是「Web 服务器」。前者懂 Python 但不会发静态文件、不会扛 TLS;后者不会跑 Python 但发文件极快。两者配合才是标准架构。

进程守护:systemd

关掉终端也不死、崩了自动重启
先讲为什么上面那条 gunicorn 命令是「前台进程」——你一按 Ctrl+C 或者一断开 SSH,它就死了。上线之后你不可能一直挂着一个终端窗口守着它。Linux 的标准做法是把它注册成 systemd 服务:系统统一托管、后台常驻、崩溃自动重启、开机自动拉起。这是「本地能跑」和「7×24 能用」之间最关键的一步。
动手做写一个 hiwen.service 单元文件。下面每一行的作用都写在注释里——照着改路径即可。
/etc/systemd/system/hiwen.serviceini
[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
效果预览运行结果
运行结果: # 每次改了 unit 文件都要 reload 一次,systemd 才会读到新内容 $ sudo systemctl daemon-reload # 启动 + 设为开机自启 $ sudo systemctl enable --now hiwen Created symlink /etc/systemd/system/multi-user.target.wants/hiwen.service → /etc/systemd/system/hiwen.service. $ sudo systemctl status hiwen --no-pager ● hiwen.service - 古典文学网后端(Gunicorn) Loaded: loaded (/etc/systemd/system/hiwen.service; enabled; preset: enabled) Active: active (running) since Tue 2026-10-15 10:30:12 CST; 8s ago Main PID: 2530 (gunicorn) Tasks: 5 (limit: 4657) Memory: 128.4M CPU: 512ms CGroup: /system.slice/hiwen.service ├─2530 /www/hiwen/venv/bin/python /www/hiwen/venv/bin/gunicorn -w 4 ... ├─2533 /www/hiwen/venv/bin/python /www/hiwen/venv/bin/gunicorn -w 4 ... ├─2534 /www/hiwen/venv/bin/python /www/hiwen/venv/bin/gunicorn -w 4 ... ├─2535 /www/hiwen/venv/bin/python /www/hiwen/venv/bin/gunicorn -w 4 ... └─2536 /www/hiwen/venv/bin/python /www/hiwen/venv/bin/gunicorn -w 4 ...
动手做记熟这几条运维命令。以后「改完代码要更新线上」,你就是靠 restart 让它生效的。
日常运维四条命令bash
# ① 启动 / 停止 / 重启
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 systemctl restart hiwen && sleep 1 && sudo journalctl -u hiwen -n 5 --no-pager Oct 15 10:33:02 server systemd[1]: Stopping 古典文学网后端(Gunicorn)... Oct 15 10:33:02 server systemd[1]: hiwen.service: Deactivated successfully. Oct 15 10:33:05 server systemd[1]: Started 古典文学网后端(Gunicorn). Oct 15 10:33:06 server gunicorn[2601]: [INFO] Listening at: http://127.0.0.1:5000 (2601) Oct 15 10:33:06 server gunicorn[2604]: [INFO] Booting worker with pid: 2604
避坑三个最容易踩的坑:① ExecStart 里写了裸命令名 gunicorn——systemd 不看你的 PATH,它会报 status=203/EXEC。必须写绝对路径 /www/hiwen/venv/bin/gunicorn。② 改了 unit 文件没 daemon-reload——你以为改了,其实 systemd 还在用旧配置,怎么重启都不生效。③ 把密码写进 ExecStart——进程列表 ps aux 里谁都能看到,要用 EnvironmentFile。

Nginx 配置

一个 80 端口,同时发前端、连后端
先讲为什么现在后端跑起来了,但它在 127.0.0.1:5000,外网访问不到;前端是一堆 dist/ 静态文件,也没人负责发。Nginx 就干这两件事:① 静态托管——用户访问 / 时把 dist 里的文件发给他;② 反向代理——用户访问 /api/ 时把请求转给 Gunicorn。这样前后端共用同一个域名,浏览器眼里全是「同源」,连同源策略、跨域配置都省了。
动手做装 Nginx,写站点配置。注意四个 location:静态托底、接口反代、上传目录、history 路由兜底。
安装 Nginxbash
sudo apt install -y nginx
sudo systemctl enable --now nginx

# 默认站点会占着 80 端口,把它停用掉,换成我们自己的配置
sudo rm -f /etc/nginx/sites-enabled/default
终端输出命令行执行结果
运行结果: $ nginx -v nginx version: nginx/1.24.0 (Ubuntu) $ systemctl status nginx --no-pager | head -3 ● nginx.service - A high performance web server and a reverse proxy server Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled) Active: active (running) since Tue 2026-10-15 10:40:01 CST; 5s ago
/etc/nginx/sites-available/hiwen.confnginx
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;
  }
}
效果预览运行结果
运行结果: # 建软链启用站点(sites-available 是「可选」,sites-enabled 才是「生效」) $ sudo ln -s /etc/nginx/sites-available/hiwen.conf /etc/nginx/sites-enabled/hiwen.conf # 改完配置先测语法,再加载 $ sudo nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful $ sudo systemctl reload nginx # reload 是「平滑重载」,不断现有连接;只有改监听端口之类的才需要 restart
讲透:每个 location 到底在解决什么
  • location / + try_files:这是 SPA 部署最不能少的一行。用户直接刷新 /article/1,Nginx 会去磁盘找叫「article/1」的文件,找不到就返回 404——加上 try_files ... /index.html 就变成「找不到就交给 index.html,让前端路由去处理」。
  • location /api/:反代的核心。不写这三行 proxy_set_header,后端日志里全是 127.0.0.1,排查问题时查不到真实用户 IP。
  • alias 与 root 的区别:alias 是「把匹配到的路径替换成这个目录」;root 是「在前面拼上」。用 /static/ 匹配时必须用 alias,且结尾的斜杠两边都要写,否则路径会拼错。
  • 为什么上传目录交给 Nginx:Flask 发一张图片要经过 Python,慢且占 worker;Nginx 直接用 sendfile 把文件甩出去,性能差一个数量级。
避坑三个必记的坑:① 忘了 client_max_body_size——Nginx 默认只允许 1M,上传 2M 头像必挂,报 413 Request Entity Too Large,而 Flask 那边日志一片空白(因为请求根本没转发过去),特别容易找错方向。② 忘了 try_files——首页正常、一刷新详情页就 404,很多人以为是 Vue Router 的问题。③ 忘了透传 Authorization 头——表现是所有需要登录的接口都返回 401,但你在本地用 Postman 测又是好的。

Apache 对照方案

教材用 Apache,这里给出等价配置
先讲为什么教材用的是 Apache,它是上一代最主流的 Web 服务器,至今仍大量存在于企业内网和旧项目里。本课程主推 Nginx(配置更直白、对前后端分离更友好、静态文件性能更好),但你必须会 Apache——面试时被问「你们用的什么?为什么不用另一个?」,答不上来就露馅了。
动手做(方案 A · 推荐)用 Apache 的 mod_proxy 做反向代理——它和 Nginx 的 proxy_pass 是同一件事,架构完全一致,Gunicorn 照旧在 127.0.0.1:5000 跑着。这也是前后端分离项目用 Apache 时的标准做法。
/etc/apache2/sites-available/hiwen.conf(方案 A · ProxyPass)apache
<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>
效果预览运行结果
运行结果: $ sudo a2enmod proxy proxy_http Enabling module proxy. Enabling module proxy_http. $ sudo a2ensite hiwen.conf Enabling site hiwen. $ sudo apache2ctl configtest Syntax OK $ sudo systemctl reload apache2 # 等价于 Nginx 的 nginx -t + systemctl reload nginx
动手做(方案 B · 教材风格)教材里通常用 Apache 自带的 mod_wsgi——不经过 Gunicorn,由 Apache 自己 fork 出进程跑 Flask。这种模式适合「Flask 单体应用(后端连页面模板一起管)」的项目。了解它的写法即可,本课程的分离式项目请优先用方案 A。
方案 B · mod_wsgi 直接托管 Flask(教材风格)apache
<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'」
效果预览运行结果
运行结果: # 方案 B 下访问的是 WSGI 挂载点,注意路径与蓝图前缀要对齐 # 我们的蓝图自带 /api 前缀(url_prefix="/api/user"), # 所以 WSGIScriptAlias 必须挂到「根路径 /」,不能再挂到 /api, # 否则会变成 /api/api/user/login 这种双重前缀。 $ curl -s http://127.0.0.1/api/category/list {"code":200,"msg":"success","data":[{"id":1,"name":"唐诗",...}]} # ── 两种方案怎么选 ── 方案 A(ProxyPass + Gunicorn):前后端分离项目的标准做法, 与 Nginx 部署架构完全一致,换 Web 服务器不用改后端。 方案 B(mod_wsgi):教材风格,适合 Flask 单体应用; 少了 Gunicorn 一层,但进程模型与 Apache 绑定, 部署分离式前端时反而别扭。
讲透:Nginx 与 Apache 的对照表(面试高频)
  • 反向代理:Nginx 的 proxy_pass ↔ Apache 的 ProxyPass(同一件事)。
  • history 路由兜底:Nginx 的 try_files $uri $uri/ /index.html ↔ Apache 的 FallbackResource /index.html。
  • 上传体积上限:Nginx 的 client_max_body_size ↔ Apache 的 LimitRequestBody。
  • 鉴权头透传:Nginx 的 proxy_set_header Authorization ↔ Apache 的 ProxyPassReverse + 默认透传(必要时还需 RequestHeader set Authorization)。
  • 一句话总结:Nginx 用「事件驱动」处理并发,静态文件更强;Apache 模块生态更老牌,靠 .htaccess 能免重启改配置。前端分离项目现在主流选 Nginx。

HTTPS(可选)

Let's Encrypt 免费证书,一条命令的事
先讲为什么HTTP 是明文传输——用户在表单里填的密码,在公共 Wi-Fi 下可以被同网段的人抓到。HTTPS 给流量加密,顺便还能防「运营商插广告」和「钓鱼网站仿冒」。现在浏览器对 HTTP 站点会直接标「不安全」,不做 HTTPS 的项目在答辩里会显得不完整。好消息是:Let's Encrypt 提供免费证书,配好只要一条命令,还会自动续期。
动手做前提:域名已解析到服务器 IP(在域名服务商后台加一条 A 记录指向你的公网 IP)。然后让 certbot 自动改 Nginx 配置。
申请并安装证书bash
# ① 确认域名解析已生效(应该返回你的公网 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
终端输出命令行执行结果
运行结果: $ sudo certbot --nginx -d example.com Saving debug log to /var/log/letsencrypt/letsencrypt.log Requesting a certificate for example.com Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem This certificate expires on 2027-01-13. These files will be updated when the certificate renews. Deploying certificate Successfully deployed certificate for example.com to /etc/nginx/sites-enabled/hiwen.conf Congratulations! You have successfully enabled HTTPS on https://example.com $ sudo certbot renew --dry-run Congratulations, all simulated renewals succeeded: /etc/letsencrypt/live/example.com/fullchain.pem (success)
讲透:HTTPS 之后要注意的三件事
  • 前端 baseURL 要跟着变:如果之前写死了 http://example.com/api,改 HTTPS 后会触发「混合内容」拦截。本项目前端用的是相对路径 /api,所以什么都不用改——这就是相对路径的好处。
  • 证书到期自动续期:Let's Encrypt 证书有效期 90 天,certbot 会装定时器自动续,别手动续。可以用 systemctl list-timers | grep certbot 确认定时器在跑。
  • 云厂商的免费证书:阿里云 / 腾讯云也提供免费 DV 证书(有效期 1 年),在控制台申请后下载 Nginx 格式,手动配 ssl_certificate 也行。图省事就用 certbot,图可控就用云厂商证书。

部署后自检清单

上线前必须逐条跑通
动手做部署完不要急着截图交作业。换一台设备、换一个网络(用手机 4G 不开 Wi-Fi)打开一遍,逐条对照下面的清单。每一项都要有「命令 + 输出」作为证据。
自检清单(含验证命令)text
[ ] 首页可开        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
效果预览运行结果
运行结果: # 一遍跑完(把 IP 换成你的域名或公网 IP) $ curl -sI http://example.com/ | head -1 HTTP/1.1 200 OK $ curl -s http://example.com/api/article/list?page=1&page_size=9 {"code":200,"msg":"success","data":{"list":[...],"total":6,"page":1,"page_size":9}} $ curl -sI http://example.com/article/1 | head -1 HTTP/1.1 200 OK # 有 try_files 兜底,刷新详情页不会 404 $ ss -lntp | grep 5000 LISTEN 0 2048 127.0.0.1:5000 0.0.0.0:* users:(("gunicorn",pid=2530,...)) # 注意是 127.0.0.1 而不是 0.0.0.0 —— 说明 Gunicorn 没有暴露在公网 $ sudo reboot # 重启后(约 30 秒),不做任何操作直接访问 http://example.com # 首页正常打开 → 开机自启配置成功
讲透:本阶段的交付物
  • 一个公网可访问的网址:http://example.com(配了证书就是 https://)。这是阶段 6 唯一的硬指标。
  • 一份部署说明文档:服务器配置、目录结构、装过的软件版本、关键配置文件的位置与内容。
  • 一份自检记录:上面每一条的验证命令与输出截图。
  • 一套可复现的部署步骤:换一台服务器,照着文档能再部署一遍——这比「我这台能跑」有价值得多。

常见部署故障排查

先看状态、再看日志
部署排错三板斧:systemctl 看状态 → journalctl 看日志 → curl 验端口① 看状态systemctl status hiflaskactive(running)?还是 failed?② 看日志journalctl -u hiflask -n 50报错栈一眼定位哪行崩了③ 验端口curl http://127.0.0.1:5000/api/Nginx 层用 curl localhost高频故障速查502 → Gunicorn 没起/端口不对 403 → 文件权限 白屏 → dist 路径/base 不对刷新 404 → 忘配 try_files 数据库连不上 → 账号密码/安全组/未启动
先讲为什么部署出错时,新手容易「凭感觉乱改」——改改 Nginx、改改 Python,越改越乱。正确的顺序是「自下而上逐层确认」:先确认 Gunicorn 起了没,再确认 Nginx 能不能访问它,最后确认外网能不能访问 Nginx。哪一层断了,错误码已经告诉你了。
动手做对照症状表定位。牢记这句口诀:状态码即线索——502 怪后端,403/404 怪路径与权限,413 怪体积限制,401 怪鉴权头。
症状 → 病因 → 药方text
症状                        病因                                药方
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 里的绝对路径可执行文件
效果预览运行结果
运行结果: # 排查 502 的标准动作(三步定位,不到一分钟) $ sudo systemctl status hiwen --no-pager | head -4 ● hiwen.service - 古典文学网后端(Gunicorn) Loaded: loaded (/etc/systemd/system/hiwen.service; enabled; preset: enabled) Active: failed (Result: exit-code) since Tue 2026-10-15 11:02:10 CST Process: 2891 ExecStart=/www/hiwen/venv/bin/gunicorn ... (code=exited, status=1/FAILURE) $ sudo journalctl -u hiwen -n 8 --no-pager Oct 15 11:02:10 server gunicorn[2891]: ModuleNotFoundError: No module named 'app' Oct 15 11:02:10 server systemd[1]: hiwen.service: Main process exited, code=exited, status=1/FAILURE # 结论:gunicorn 找不到 app 模块 —— WorkingDirectory 写错了(写成了 /www/hiwen, # 而代码在 /www/hiwen/hiflask)。改回来重新 daemon-reload + restart 即可。
讲透:自下而上的排查顺序
  • 第一层:Gunicorn 活着吗?systemctl status hiwen 看是不是 active (running),不是就 journalctl 看堆栈。
  • 第二层:本机能访问到它吗?curl http://127.0.0.1:5000/api/category/list。这一步能通,说明后端没问题,问题在 Nginx。
  • 第三层:Nginx 能转发到它吗?在服务器上 curl http://127.0.0.1/api/category/list(不带端口,走 80)。通了说明 Nginx 配置对,问题在外网入口。
  • 第四层:外网能访问吗?在你自己电脑上 curl 域名。不通就查安全组、ufw、域名解析。
  • 口诀:状态码即线索——502 怪后端、403/404 怪路径与权限、413 怪体积上限、401 怪鉴权头。不要「凭感觉改配置」,先让错误码告诉你哪一层断了。
避坑两条纪律:① 改配置前先备份——sudo cp /etc/nginx/sites-available/hiwen.conf{,.bak},出问题时能一键回滚;线上手忙脚乱地「边改边试」,很容易把能跑的服务改挂。② 别在生产库上跑初始化脚本 / db.drop_all()。生产库里是真实数据,一条 drop 下去就没了,只能靠备份恢复。要重置数据,先在本地库演练一遍。
动手练习
1把「古典文学网」部署到一台云服务器上,跑通自检清单里全部 10 项,并给出每项的命令与输出截图。要求:绑定一个域名(可用免费的二级域名),Gunicorn 只监听 127.0.0.1。
2模拟一个真实故障:手工把 hiwen.service 里的 WorkingDirectory 改错,重启服务,记录下你看到的错误码与日志;再改回来验证恢复。写出你定位这个故障的完整过程(每一层查了什么、看到了什么)。
3迁移练习:「红影赏析」需要部署到另一台机器上,除了域名换成 movie.example.com,还多了一个需求:电影的预告片视频(单个最大 50MB)要支持上传。请你写出需要改动的 nginx 配置片段(至少涉及 client_max_body_size 与一个新的上传目录 location),并说明为什么 Flask 端也要同步调整 MAX_CONTENT_LENGTH。想一想:这跟部署「古典文学网」的差别在哪?
参考答案
第 1 题 · 关键配置文件对照text
# 部署完成后的「四件套」,你交的说明文档里应该能一一对应
[ ] /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
效果预览运行结果
运行结果: # 最终交付的那个网址,在手机上(用 4G 流量,不开 Wi-Fi)打开 http://example.com 首页:轮播图 1 张 + 分类 5 个 + 推荐 6 篇,全部正常 列表:筛选「宋词」→ 2 篇;刷新页面不 404 详情:浏览量 +1;书评区 2 条一级 + 1 条回复 登录:登录成功,顶栏显示昵称,F5 不掉登录态 上传:换头像成功,图片正常显示 -- 阶段 6 达标:一个真正公网可访问的网址
第 2 题 · 故障定位过程text
【现象】访问 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":[...]}      ← 恢复
效果预览运行结果
运行结果: # 这次故障的「一句话复盘」 502 不是 Nginx 的问题,是后端没起来;后端没起来的原因在日志里写得清清楚楚 (ModuleNotFoundError);顺着「gunicorn 在哪找模块」这条线,就查到了 WorkingDirectory。 # 关键教训:改完 unit 文件必须 daemon-reload + restart,且报错先看日志再动手改配置
第 3 题 · 红影赏析的上传扩展nginx
# /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;
  }
}
效果预览运行结果
运行结果: # Flask 端必须同步改:config.py 里的 MAX_CONTENT_LENGTH MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB # 为什么两层都要改?(这是第 3 题的答题要点) 1)Nginx 的 client_max_body_size:在请求「还没进后端」时就拦。 不改它,请求直接被 Nginx 掐掉,返回 413,Flask 日志里连一条记录都没有 —— 你会误以为「后端没收到请求」,其实是根本没转发过去。 2)Flask 的 MAX_CONTENT_LENGTH:在请求「进了后端」后校验。 不改它,Nginx 放行了,但 Flask 收到超过限制的请求体会抛 413 RequestEntityTooLarge, 表现为 500 或 413。 → 一层管「准入」,一层管「兜底」,缺一个都做不成大文件上传。 # 和部署「古典文学网」的差别: - 差别 1:体积上限。头像 2M → 视频 50M,Nginx 与 Flask 两处上限都要放大。 - 差别 2:超时。大文件上传是长连接,read/send timeout 要从 60s 提到 300s。 - 差别 3:存储位置。视频体积大、IO 重,一定放独立目录交给 Nginx 直出, 绝不能让 Flask(或 Gunicorn worker)去读几十兆的文件 —— 那会占满 worker。 不变的是:静态托管 + /api 反代 + try_files 兜底这套骨架,一模一样。

第 3 题的核心:文件越大,越要「早拦、少经过 Python」。Nginx 负责在门口挡住超限请求,Flask 负责兜底校验,大文件由 Nginx 直接发送。能说清「为什么两层都要改」,说明你理解了请求在这一整条链路上到底经过了谁。

本阶段小结