Nginx 部署前端 + 反向代理后端

1. 为什么是 Nginx

部署前端静态文件的工具多如牛毛(Nginx、Caddy、Apache、Lighttpd)。Nginx 胜在:

Java 对照:后端的 Tomcat 可以直接托管静态文件,但生产上不这么干——让 Nginx 挡在前面,Tomcat 只跑业务逻辑。前端部署遵循同样的分层思路。


2. 最小可用配置

假设你有台 Ubuntu 服务器,安装 Nginx:

sudo apt update
sudo apt install nginx

构建好 Vue 项目(pnpm build),把 dist/ 拷到服务器的 /var/www/in4vue/

# 本地
pnpm build
rsync -av dist/ user@server:/var/www/in4vue/

写一个站点配置 /etc/nginx/sites-available/in4vue

server {
    listen 80;
    server_name in4vue.example.com;

    root /var/www/in4vue;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

启用并重载:

sudo ln -s /etc/nginx/sites-available/in4vue /etc/nginx/sites-enabled/
sudo nginx -t          # 校验配置语法
sudo systemctl reload nginx

访问 http://in4vue.example.com/,Vue 项目就跑起来了。


3. try_files:SPA 的灵魂

Vue Router 的 createWebHistory 是"假路由"——/notes/123 这个 URL 在服务器上没有对应文件。用户刷新这个页面时,Nginx 默认返 404。

try_files $uri $uri/ /index.html 的含义:

  1. 先找叫 $uri(如 /notes/123)的文件
  2. 找不到,找 $uri/ 这个目录
  3. 还找不到,全部返回 /index.html

返回 index.html 后,Vue Router 在客户端接管,按 /notes/123 匹配路由。缺了这行,刷新任何内页都 404

Java 对照:类似 Spring MVC 的"所有未匹配路径都交给前端控制器"——把路由控制权交给客户端。


4. 反向代理后端 API

前端项目 /api/* 请求要打到后端。一种方式是前端代码里写死后端地址(开 CORS),更推荐 Nginx 反代

server {
    listen 80;
    server_name in4vue.example.com;

    # 前端
    root /var/www/in4vue;
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 后端 API
    location /api/ {
        proxy_pass http://127.0.0.1:8080/;
        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;
    }
}

一刀解决跨域:对浏览器来说一切都是 in4vue.example.com,同域。

4.1 proxy_pass 末尾的斜杠

# A: target 带斜杠 → 吃掉 location 前缀
location /api/ {
    proxy_pass http://127.0.0.1:8080/;
}
# /api/notes → http://127.0.0.1:8080/notes

# B: target 不带斜杠 → 保留前缀
location /api/ {
    proxy_pass http://127.0.0.1:8080;
}
# /api/notes → http://127.0.0.1:8080/api/notes

怎么选:后端有 context-path: /api(Spring Boot 设的)选 B;后端接口不带前缀选 A。

4.2 proxy_set_header 为什么要写

后端 Spring 代码里可能有:

String clientIp = request.getRemoteAddr();

不加这些 headerclientIp 永远是 127.0.0.1(Nginx 的地址)。 加了 X-Real-IP / X-Forwarded-For:Spring 从 header 里拿到真实用户 IP。

业务里要从这些 header 取:

String realIp = request.getHeader("X-Real-IP");

或者配 Spring 的 ForwardedHeaderFilter 自动识别。


5. HTTPS:Let's Encrypt + certbot

HTTPS 是现代部署标配。浏览器对 http:// 越来越不友好(密码输入框警告、geolocation 不给用)。

用 Let's Encrypt 免费证书

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d in4vue.example.com

certbot 会:

  1. 自动验证你对域名的控制权
  2. 申请证书存到 /etc/letsencrypt/live/in4vue.example.com/
  3. 自动改 Nginx 配置加 HTTPS 和自动跳转
  4. 设定证书 90 天自动续期(/etc/cron.d/certbot

证书就位后配置大致变成:

server {
    listen 80;
    server_name in4vue.example.com;
    return 301 https://$host$request_uri;  # HTTP 强跳 HTTPS
}

server {
    listen 443 ssl http2;
    server_name in4vue.example.com;

    ssl_certificate /etc/letsencrypt/live/in4vue.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/in4vue.example.com/privkey.pem;

    # HSTS:告诉浏览器一年内强制 HTTPS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    root /var/www/in4vue;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8080/;
        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;
    }
}

http2:HTTP/2 支持。一条连接走多路请求,比 HTTP/1.1 快很多。免费开。


6. gzip / brotli 压缩

传输大小减半的快速优化:

http {
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 1024;
    gzip_types
        text/plain
        text/css
        text/xml
        application/javascript
        application/json
        application/xml
        image/svg+xml;
}

brotli 比 gzip 再压缩 20%,但 Nginx 默认没编译。Ubuntu 下:

sudo apt install libnginx-mod-brotli

配置:

brotli on;
brotli_comp_level 6;
brotli_types
    text/css
    application/javascript
    application/json;

效果:JS/CSS 传输 500KB → 约 120KB。


7. 静态资源缓存策略

Vite 构建的资源文件名带 hash(index-a1b2c3.js),修改一点内容 hash 就变。这意味着:

# /assets/ 下带 hash 的文件,一年长缓存
location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

# 其他静态资源(图片、字体)
location ~* \.(ico|png|jpg|jpeg|gif|svg|woff2?)$ {
    expires 7d;
    add_header Cache-Control "public";
}

# index.html 不缓存
location = /index.html {
    expires -1;
    add_header Cache-Control "no-cache, no-store, must-revalidate";
}

immutable 是个关键词 —— 告诉浏览器"就算用户按 F5,也别再来问一次,直接用本地缓存"。大幅减少不必要的 304 请求


8. 安全头

基础防御几个常见攻击,全加:

# 防止页面被其他站点 iframe(点击劫持)
add_header X-Frame-Options "SAMEORIGIN" always;

# 防止浏览器做 MIME 嗅探
add_header X-Content-Type-Options "nosniff" always;

# Referrer 只传必要信息
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# 禁用一些浏览器特性 API
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

# HTTPS 下加(第 5 节已加 HSTS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

CSP(Content-Security-Policy)更严格但配置复杂,留到安全笔记详细讲。


9. 多环境虚拟主机

一台服务器挂 test.in4vue.com / staging.in4vue.com / in4vue.com——三个独立站点:

# /etc/nginx/sites-available/in4vue-prod
server {
    listen 443 ssl http2;
    server_name in4vue.com;
    root /var/www/in4vue-prod;
    # ...
}

# /etc/nginx/sites-available/in4vue-staging
server {
    listen 443 ssl http2;
    server_name staging.in4vue.com;
    root /var/www/in4vue-staging;

    # 加基础认证,不让外人访问
    auth_basic "Staging";
    auth_basic_user_file /etc/nginx/.htpasswd;

    # ...
}

启用:

sudo ln -s /etc/nginx/sites-available/in4vue-prod /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/in4vue-staging /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Java 对照:类似用 Spring Profile 起多个实例,nginx 的多 server 块 = 多套独立配置。


10. 零停机发布

单机部署最头疼的是"发版瞬间用户看到旧版 index.html 但新 hash 的 JS 已经没了"。方案:

10.1 原子切换

# 发布流程
cd /var/www
mkdir in4vue-new
rsync -av dist/ in4vue-new/
mv in4vue in4vue-old
mv in4vue-new in4vue
rm -rf in4vue-old

mv 是原子操作(同磁盘下),切换瞬间完成。

10.2 保留旧 hash 资源几分钟

关键技巧:新版本部署时不删旧版本的 /assets/*.js。用户已打开的老页面还能正常下带旧 hash 的文件。几分钟后再清理。

# 合并新旧 assets(保留旧文件)
rsync -av dist/ /var/www/in4vue/ --exclude='index.html'
cp dist/index.html /var/www/in4vue/index.html.new
mv /var/www/in4vue/index.html.new /var/www/in4vue/index.html

# 定时任务清 30 天以上没访问的 assets
find /var/www/in4vue/assets -atime +30 -delete

10.3 灰度发布(多实例)

后面 CI/CD 笔记会讲。


11. 日志

默认 Nginx 日志:

11.1 自定义格式

前端通常只关心请求,加个 JSON 格式便于 ELK / Loki 采集:

log_format json_combined escape=json
    '{'
        '"time":"$time_iso8601",'
        '"remote_addr":"$remote_addr",'
        '"method":"$request_method",'
        '"uri":"$request_uri",'
        '"status":$status,'
        '"body_bytes_sent":$body_bytes_sent,'
        '"referer":"$http_referer",'
        '"user_agent":"$http_user_agent",'
        '"response_time":$request_time'
    '}';

server {
    access_log /var/log/nginx/in4vue-access.log json_combined;
}

11.2 日志切割

Ubuntu 自带 logrotate/etc/logrotate.d/nginx 已经配好每天切割 + 保留 14 天。无需手动维护。


12. 限流

防暴力请求,加 limit_req

http {
    # 定义:按 IP,每秒 10 个请求,突发 20
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
}

server {
    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        proxy_pass http://127.0.0.1:8080/;
    }
}

Java 对照:类似 Spring 的 @RateLimiter(Resilience4j)——但 Nginx 挡在前面更省后端资源。


13. 完整 in4vue 生产配置

把前面所有东西合起来:

# /etc/nginx/sites-available/in4vue

# HTTP → HTTPS
server {
    listen 80;
    server_name in4vue.example.com;
    return 301 https://$host$request_uri;
}

# HTTPS 主站
server {
    listen 443 ssl http2;
    server_name in4vue.example.com;

    ssl_certificate /etc/letsencrypt/live/in4vue.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/in4vue.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers off;

    root /var/www/in4vue;
    index index.html;

    # 安全头
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # 压缩
    gzip on;
    gzip_vary on;
    gzip_comp_level 6;
    gzip_min_length 1024;
    gzip_types text/css application/javascript application/json image/svg+xml;

    # SPA 回退
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 长缓存资源
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # index.html 不缓存
    location = /index.html {
        expires -1;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }

    # 后端 API
    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        proxy_pass http://127.0.0.1:8080/;
        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;
    }

    # WebSocket(如果用到)
    location /ws {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400s;
    }

    # AI SSE(关闭缓冲,见 SSE 笔记)
    location /ai/ {
        proxy_pass http://127.0.0.1:8787/;
        proxy_buffering off;
        proxy_cache off;
        proxy_http_version 1.1;
        proxy_read_timeout 600s;
    }

    # 禁止访问隐藏文件
    location ~ /\. {
        deny all;
    }
}

api_limit zone 定义加到 /etc/nginx/nginx.confhttp {} 块里:

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

14. 常见坑点

现象 原因 解法
刷新内页 404 没配 try_files 第 3 节
后端返回的 IP 都是 127.0.0.1 没设 X-Real-IP 第 4 节 proxy_set_header
HTTPS 跳转循环 后端也在 301 后端关掉它自己的 HTTPS 跳转
文件上传超过大小限制 Nginx 默认 1M client_max_body_size 50m;
502 Bad Gateway 后端挂了或端口错 error.log 查具体原因
WebSocket 连不上 没配 Upgrade 头 第 13 节 /ws
Gzip 对某些文件没生效 gzip_types 没列 检查 Content-Type 和 types 列表
图片大但不压缩 JPEG / PNG 已是压缩格式,gzip 意义不大 不用压缩图片,靠 WebP / AVIF 格式优化

15. 调试三板斧

# 1. 语法校验(改完必跑)
sudo nginx -t

# 2. 看错误日志
sudo tail -f /var/log/nginx/error.log

# 3. 看访问日志
sudo tail -f /var/log/nginx/access.log

# 热重载(不中断请求)
sudo systemctl reload nginx

# 完全重启(罕用)
sudo systemctl restart nginx

调接口失败先看 Nginx 日志,再看后端日志——顺着链路往下查


16. 心智模型

用户请求
   ↓
DNS 解析 → 服务器 IP
   ↓
[Nginx 443]
   ├─ /                  → 静态文件(Vue SPA)
   ├─ /api/*             → 反代到后端 Spring Boot
   ├─ /ws                → 反代 WebSocket
   ├─ /ai/*              → 反代 AI 代理(关缓冲)
   └─ /assets/*          → 长缓存资源

职责:
  Nginx = 门卫 + 静态服务器 + 反向代理 + 压缩 + HTTPS
  Java 后端 = 业务逻辑
  前端 dist/ = 文件系统里的 HTML/JS/CSS

小练习

  1. 本地起一个 Nginx(Docker 或 Ubuntu WSL),按第 2 节跑起 dist/
  2. try_files,测刷新任意内页是否 200
  3. 配一个假后端(httpd:alpine 容器或 Python http.server),用 /api 反代
  4. 加 gzip,用 DevTools Network 看响应头 Content-Encoding: gzip
  5. certbot --nginx 在有域名的服务器上申请一次证书

延伸阅读