06. 使用 Nginx 托管前端和代理 API
1. 本章目标
本章让 Nginx 返回 Vue 构建产物,并把 /api/ 和 /health 请求代理到 127.0.0.1:3000。完成后浏览器只访问 https://airport.cxhair.cn,看不到内部端口。
2. 构建前端
cd /var/www/airport-operations-explorer
npm ci
npm run build -w client
find client/dist -maxdepth 2 -type f | head -n 20
client/dist/index.html 是 SPA 入口,client/dist/assets/ 包含带内容哈希的 JavaScript 和 CSS。不要把整个仓库设为 Nginx root,否则可能暴露源码或不应公开的文件。
确保 Nginx 进程可以沿路径读取文件:
namei -l /var/www/airport-operations-explorer/client/dist/index.html
目录通常至少需要执行权限,文件需要读取权限。不要简单使用 chmod -R 777。
3. 完整站点配置
先用以下命令确认实际证书路径:
sudo certbot certificates
创建或编辑 /etc/nginx/sites-available/airport.cxhair.cn。下面假设共享证书名称为 cxhair.cn:
server {
listen 80;
listen [::]:80;
server_name airport.cxhair.cn;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name airport.cxhair.cn;
root /var/www/airport-operations-explorer/client/dist;
index index.html;
ssl_certificate /etc/letsencrypt/live/cxhair.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cxhair.cn/privkey.pem;
# Certbot 生成的服务器可能还包含这些安全参数。
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
access_log /var/log/nginx/airport.access.log;
error_log /var/log/nginx/airport.error.log;
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.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;
proxy_set_header X-Request-ID $request_id;
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
location = /health {
proxy_pass http://127.0.0.1:3000/health;
proxy_http_version 1.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;
proxy_set_header X-Request-ID $request_id;
proxy_connect_timeout 5s;
proxy_send_timeout 5s;
proxy_read_timeout 5s;
}
location /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
}
如果 certbot certificates 显示独立证书名称为 airport.cxhair.cn,把两个证书路径改为 /etc/letsencrypt/live/airport.cxhair.cn/...。不要创建证书文件副本,始终引用 Certbot 管理的 live 路径。
较旧 Nginx 不支持单独的 http2 on;。如果 nginx -t 报 unknown directive,可使用:
listen 443 ssl http2;
listen [::]:443 ssl http2;
以服务器已安装版本的语法为准。
4. 理解关键配置
server_name
Nginx 根据监听地址和 Host 选择 server。server_name airport.cxhair.cn 防止请求落入默认站点。证书正确但仍看到别的网站时,应首先检查虚拟主机匹配。
root
对 /assets/app.js,Nginx 将 URI 拼到 root 后,读取:
/var/www/airport-operations-explorer/client/dist/assets/app.js
Vue Router 的 try_files
浏览器直接刷新 /flights 时,磁盘上没有 dist/flights。这条配置让 Nginx 回退到 index.html,再由 Vue Router 解释路由:
try_files $uri $uri/ /index.html;
不要把 API location 放在这个回退规则之后而依赖顺序碰运气。Nginx 会按 location 匹配规则选择配置,/api/ 要有自己的 location。
proxy_pass 的尾部斜杠
当前配置:
location /api/ {
proxy_pass http://127.0.0.1:3000;
}
请求 /api/flights 会以 /api/flights 转发,符合项目 API 路径规划。
若写为:
proxy_pass http://127.0.0.1:3000/;
Nginx 会用 / 替换匹配到的 /api/,后端收到 /flights。这不是谁更好,而是必须与 Express 路由契约一致。本项目保留 /api,所以 proxy_pass 后没有 URI 斜杠。
代理头
Host:保留浏览器访问的域名;X-Real-IP:记录直接客户端地址;X-Forwarded-For:追加代理链地址;X-Forwarded-Proto:告诉应用原始请求是 HTTPS;X-Request-ID:把 Nginx 请求 ID 传给 Express,便于关联日志。
当前 Express 会接受长度不超过 128 的 X-Request-ID。只有确认应用始终位于可信 Nginx 后面时,才考虑设置 Express trust proxy。不要因为存在 X-Forwarded-For 就无条件信任来自公网的该请求头。
缓存
Vite 的静态资源文件名包含内容哈希,内容变化后文件名变化,可以缓存一年。index.html 指向最新资源,不应使用同样的长期 immutable 缓存。
超时
连接后端只给 5 秒,普通 API 读取最多 30 秒。超时不是性能优化;大查询仍应设置日期和条数上限。若某个分析接口合理需要更长时间,应先记录查询预算,再针对该 location 调整,而不是全局设置数小时。
5. 启用并验证配置
sudo ln -s /etc/nginx/sites-available/airport.cxhair.cn \
/etc/nginx/sites-enabled/airport.cxhair.cn
sudo nginx -t
sudo systemctl reload nginx
每次都先执行 nginx -t。语法错误时不要 reload。
若 Ubuntu 默认站点抢占请求,可以在确认没有其他用途后移除它的启用链接:
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
这只删除 sites-enabled 中的软链接,不删除 /etc/nginx/sites-available/default 原文件。
6. 分层验证
后端本机
curl -i http://127.0.0.1:3000/health
Nginx 本机 HTTPS
curl -I https://airport.cxhair.cn/
curl -i https://airport.cxhair.cn/health
SPA 路由
curl -I https://airport.cxhair.cn/flights
应返回 HTML,而不是 Nginx 404。
静态缓存
先从 index.html 找到真实资源名,再检查:
curl -I https://airport.cxhair.cn/assets/<实际文件名>.js
应看到长期缓存头。
API 路径
当前初始化工程只有 /health,后续加入 /api 路由后执行:
curl -i https://airport.cxhair.cn/api/<实际接口>
不要用不存在的路径期待 200;404 也可以证明请求已经到达 Express,结合 JSON 错误结构和 request ID 判断。
7. 查看日志
sudo tail -f /var/log/nginx/airport.access.log
sudo tail -f /var/log/nginx/airport.error.log
pm2 logs airport-api
Nginx 返回 502 时通常无法连接上游;先测试 127.0.0.1:3000,再检查 PM2。返回 504 表示已连接上游但等待超时,要调查具体请求和 SQL。
8. 本章验收
- HTTP 自动跳转 HTTPS;
/返回 Vue 应用;/flights刷新不产生 Nginx 404;/health到达 Express;/api/*保留/api前缀转发;- 静态资源有合理缓存;
- 公网不能直连 3000。
下一章:第一次完整上线