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 斜杠。

代理头

当前 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. 本章验收

下一章:第一次完整上线

9. 参考资料