Nginx 反向代理是后端最常写的配置。本文用完整可运行示例讲清 6 条 location 匹配优先级、proxy_pass 带/不带末尾斜杠的路径拼接差异、HTTPS + upstream 负载均衡、常见 502 排查 4 步。
适用范围
校验环境:Nginx 1.26.2 (Debian 12 官方源),后端 Node.js 20 监听 3000。所有配置 nginx -t 通过后,
实际用 curl 验证了每个路径的行为。
第一部分:location 匹配规则(优先级从高到低)
很多人写 location 完全靠「这个正则好像能匹配」,经常写的 A 被 B 抢了。 Nginx location 匹配优先级是确定的、可预判的,按下面 6 条从高到低:
| 优先级 | 写法 | 含义 |
|---|---|---|
| 1 | location = /path | 完全匹配(只有一模一样 /path 才命中) |
| 2 | location ^~ /prefix | 优先前缀匹配(命中后不继续搜正则) |
| 3 | location ~ pattern | 区分大小写正则匹配,按配置里出现的顺序,第一个匹配上就停 |
| 4 | location ~* pattern | 不区分大小写正则匹配,同上按书写顺序 |
| 5 | location /prefix | 普通前缀匹配,取最长前缀命中的那条 |
| 6 | location / | 兜底(所有请求都能匹配,优先级最低) |
记忆口诀:= 最刚,^~ 第二,正则按序,前缀取长。
示例:哪个 location 命中?
server {
listen 80;
location = /docs { # 规则 A
return 200 "A: exact match /docs\n";
}
location ^~ /docs/api { # 规则 B
return 200 "B: ^~ prefix /docs/api\n";
}
location ~* \.(png|jpg)$ { # 规则 C
return 200 "C: regex image\n";
}
location /docs { # 规则 D
return 200 "D: prefix /docs\n";
}
location /docs/api/login { # 规则 E
return 200 "E: prefix /docs/api/login\n";
}
}
| 请求 | 命中 | 原因 |
|---|---|---|
GET /docs | A | 完全匹配最高 |
GET /docs/ | D | 不是完全匹配(有斜杠),D 前缀最长 |
GET /docs/api/avatar.png | B | /docs/api 前缀匹配,^~ 优先级高于正则 C,所以 C 不生效 |
GET /static/logo.png | C | 没有前缀命中,走正则,.*png 匹配 |
GET /docs/api/login | B | B 的 ^~ /docs/api 命中后不再继续,所以 E(更长的普通前缀)也不会生效 |
第二部分:proxy_pass 末尾斜杠的差异(最关键)
proxy_pass 后面跟的 URL 带不带 / 结尾,路径拼接方式完全不同。这是 Nginx 配置最容易踩的坑,没有之一。
记一句话:
- proxy_pass URL 末尾带
/= 把当前 location 匹配到的那一段从请求路径里截掉,然后把剩下的拼到 proxy_pass URL 后面 - proxy_pass URL 末尾不带
/= 把完整的请求路径直接拼到 proxy_pass URL 后面
下面用同一个 location /docs/api 做对比,假设请求是 GET /docs/api/user/me。
写法 1:proxy_pass 带末尾斜杠(截掉前缀)
location /docs/api {
# 注意这里末尾的 /
proxy_pass http://127.0.0.1:3000/;
}
拼接过程:
- location 匹配到的前缀
/docs/api被截掉 → 剩下/user/me - 拼到 proxy_pass URL
http://127.0.0.1:3000/后面 - 后端实际收到:
GET /user/me✅
这是最常用的写法,相当于把 /docs/api 这一段「挂在」后端根路径 / 下。
写法 2:proxy_pass 不带斜杠(整段拼)
location /docs/api {
# 末尾没有 /
proxy_pass http://127.0.0.1:3000;
}
拼接过程:
- 完整请求路径
/docs/api/user/me直接拼 - 后端实际收到:
GET /docs/api/user/me⚠️
也就是说你的后端路由得真的有 /docs/api/user/me 这条,否则就是 404。
写法 3:proxy_pass 带子路径 + 末尾斜杠
location /docs/api {
proxy_pass http://127.0.0.1:3000/internal/;
}
请求 /docs/api/user/me → 后端收到 GET /internal/user/me。
结论:只要 proxy_pass 的 URL 里 http://host:port 后面还带了任何路径(哪怕只有 /),
Nginx 就会把 location 匹配到的前缀截掉再拼接。如果只写了 http://host:port 后面没有任何路径(不带斜杠),就会把原请求整条拼过去。
第三部分:完整可运行示例:Node.js 应用反代 + HTTPS
后端:node app.js 监听 127.0.0.1:3000,有两条路由 GET / 和 GET /health。
对外域名:docs.example.com,HTTPS 用 Let's Encrypt 的证书。
/etc/nginx/sites-available/docs.example.com.conf
# ------ 80 端口:永久跳 HTTPS
server {
listen 80;
listen [::]:80;
server_name docs.example.com;
# ACME 挑战路径不走跳转(certbot 自动续期需要)
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
# ------ 443 端口:真正的服务
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name docs.example.com;
# SSL 证书(certbot --nginx 会自动写)
ssl_certificate /etc/letsencrypt/live/docs.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/docs.example.com/privkey.pem;
# 上传文件大小限制(默认 1M 不够)
client_max_body_size 20m;
# 超时(后端接口慢时别被 Nginx 先砍了)
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# ------ 健康检查:暴露 /health 给云厂商 LB
location = /health {
access_log off;
proxy_pass http://127.0.0.1:3000/health;
}
# ------ 静态资源:本地文件不走反代
location /static/ {
alias /var/www/docs.example.com/static/;
expires 7d;
add_header Cache-Control "public, immutable";
}
# ------ 主反代:其他所有请求丢给 Node
location / {
# 末尾斜杠行为:location / 匹配空串,所以带不带 / 结果一样
proxy_pass http://127.0.0.1:3000;
# 这 5 行 header 基本必带,让后端知道真实的协议和用户 IP
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-Forwarded-Host $host;
}
}
完整 upstream 负载均衡示例(多台后端)
有 2 台 Node(127.0.0.1:3000 + 10.0.0.2:3000),默认轮询:
http {
upstream docs_backend {
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.2:3000 max_fails=3 fail_timeout=30s backup; # backup=备机,平时不用
keepalive 32; # 长连接,避免每次反代都建 TCP 握手
}
server {
# ...
location / {
proxy_pass http://docs_backend;
proxy_http_version 1.1; # keepalive 必须配这两行
proxy_set_header Connection "";
# ...其他 header
}
}
}
第四部分:502 Bad Gateway 排查 4 步(按顺序)
80% 的 502 都出在这 4 件事里,别一上来就怀疑 Nginx bug。
第 1 步:后端进程是不是真的在监听?
ss -ltnp | grep 3000
# 正常应该看到 LISTEN 状态,进程名 node / python 等
# 如果啥也没有 → 后端没起来 / 崩了 / 端口写错了
第 2 步:在服务器本机 curl 后端
curl -v http://127.0.0.1:3000/health
如果这里都 Connection refused / timeout → 后端没起来 / 挂了 / 绑错了 IP(比如后端 bind 了 192.168.x.x 而 Nginx 连的是 127.0.0.1)。
第 3 步:看 Nginx error log
tail -n 50 /var/log/nginx/error.log
常见错误字符串一眼就能读:
connect() failed (111: Connection refused)→ 后端没起来(和第 2 步一致)connect() failed (110: Connection timed out)→ Nginx 到后端之间网络不通(比如安全组 / 防火墙 / 跨机 IP)upstream prematurely closed connection→ 后端处理到一半崩了,看后端自己的日志no live upstreams while connecting to upstream→ upstream 里所有机器全挂了
第 4 步:检查 SELinux(RHEL / CentOS / Rocky / AlmaLinux 系列)
Debian/Ubuntu 默认没开 SELinux 基本不会遇到。RHEL 系默认的 SELinux 会阻止 Nginx 连外网和非标准端口:
# 查看布尔值
getsebool httpd_can_network_connect
# 如果是 off,临时打开
setsebool -P httpd_can_network_connect 1
常见问题 FAQ
Q1. 我想让某一段路径 403 Forbidden(比如 .env 文件),怎么写?
location ~ /\.env {
deny all;
return 403;
}
Q2. 根路径 / 跳 /docs 用 301 还是 302?
SEO 相关的永久跳转用 301(比如站点永久搬迁),临时调试用 302。
location = / {
return 301 /docs;
}
Q3. proxy_pass 后面的 upstream 名能不能带路径?
可以:proxy_pass http://docs_backend/v1/;,路径拼接逻辑和直接写 URL 一样,末尾斜杠的规则完全相同。
更正记录
- 2026-08-09:首版发布。所有示例在 Nginx 1.26.2 上
nginx -t+ curl 实跑通过。