Nginx 部署前端 + 反向代理后端
1. 为什么是 Nginx
部署前端静态文件的工具多如牛毛(Nginx、Caddy、Apache、Lighttpd)。Nginx 胜在:
- 性能强:C 写的,单机几万 QPS
- 生态广:几乎所有运维都熟
- 文档全:遇到问题十年内的老答案都还有效
- 反向代理强:能把前端 + 后端 + 静态资源拼在一个域名下
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 的含义:
- 先找叫
$uri(如/notes/123)的文件 - 找不到,找
$uri/这个目录 - 还找不到,全部返回
/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();
不加这些 header:clientIp 永远是 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 会:
- 自动验证你对域名的控制权
- 申请证书存到
/etc/letsencrypt/live/in4vue.example.com/ - 自动改 Nginx 配置加 HTTPS 和自动跳转
- 设定证书 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 就变。这意味着:
- 带 hash 的文件永不变,浏览器可长期缓存
index.html是唯一入口,必须短缓存(不然发版用户看不到新版本)
# /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 日志:
/var/log/nginx/access.log—— 访问日志/var/log/nginx/error.log—— 错误日志
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.conf 的 http {} 块里:
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
小练习
- 本地起一个 Nginx(Docker 或 Ubuntu WSL),按第 2 节跑起
dist/ - 加
try_files,测刷新任意内页是否 200 - 配一个假后端(
httpd:alpine容器或 Pythonhttp.server),用/api反代 - 加 gzip,用 DevTools Network 看响应头
Content-Encoding: gzip - 用
certbot --nginx在有域名的服务器上申请一次证书
延伸阅读
- Nginx 官方文档
- DigitalOcean - Nginx 教程
- Mozilla SSL Configuration Generator(帮你生成安全的 SSL 配置)
- Let's Encrypt
- Nginx 实战指南(陶辉)(中文好书)