Nginx location 与 proxy_pass:最容易破坏部署的斜杠规则
Nginx 是俄罗斯工程师 Igor Sysoev 厌倦了 Apache 每连接派生一个进程之后,用事件驱动 I/O 从头重写的产物。首个公开版本 2004 年 10 月。今天 nginx 在前 100 万网站里承担的份额比任何其他服务器都大,作为 Kubernetes ingress 的反向代理在前线工作,并且是 OpenResty、Tengine、Cloudflare 自家边缘背后的底料。配置语言 2002 年被设计为“易读”——2026 年它最知名的反而是那一小撮没被修过的、专门坑住运维的歧义。
Nginx 第一次写正确出了名地难,因为它的配置语言表面上像一个普通的 key value; 配置,语义却是个小型声明式 DSL,规则你被咬过才知道。本文是这些咬痕的导览。
架构,简版
Apache 的传统模型:每连接一进程(或一线程)。请求来,分配一个 worker 处理它的整个生命周期,包括所有阻塞 I/O。一万并发就是一万 worker,内核调度成瓶颈。
Nginx 的模型:少量 worker 进程(通常一核一个),每个跑一个事件循环。每个 worker 用 epoll(Linux)/ kqueue(BSD)/ IOCP(Windows)多路复用,并发处理数千连接。当某连接没事做(等网络、等磁盘)时,worker 切到另一个连接而不是阻塞。
实际后果:同样的 RAM,nginx 比 Apache 撑得起多得多的连接,单核机器只跑静态文件就能打满千兆带宽。代价是所有阻塞工作要么卸到 worker 池(aio threads),要么推到上游。误配的 auth_request 打到一个慢后端会饿死整个 worker,让 nginx 表现得比应有的差。
配置文件结构
nginx 配置是层次的。顶层("main" 上下文)配置 worker 与全局设置。events 上下文配事件循环。http 里定义 server 块(每个虚拟主机一个),每个 server 内 location 块(每个 URL 前缀或模式一个)。
events { ... }
http {
upstream api { ... }
server {
listen 443 ssl;
server_name example.com;
location / { ... }
location /api/ { proxy_pass http://api/; }
}
}
指令从外向内继承,除非被覆盖。有些指令只能在某些上下文里用。把指令放错上下文时报错统一糟糕("unknown directive");只有文档是哪条指令支持哪些上下文的权威。
location 匹配:所有人都被坑过的规则
这是 nginx 最容易被误解的部分,也是大多数生产配置在悄悄出错的地方。
一个 server 块可以有多个 location 块。请求来时 nginx 挑选恰好一个来处理,按以下顺序:
- 精确匹配(
=修饰符)立即胜出。location = /healthz { ... } - 前缀匹配(
^~修饰符)—— 如果最长这种前缀匹配,它胜出(跳过正则匹配)。location ^~ /static/ { ... } - 正则匹配(
~大小写敏感、~*不敏感)。第一个匹配的正则胜出,按它们在配置里出现的顺序。location ~* \.(jpg|png|gif)$ { ... } - 普通前缀匹配(无修饰符)。如果没正则匹配,最长普通前缀胜出。
三种失败模式:
- 两个前缀 location,最长那个赢。
location /api/v1/users击败location /api/v1/。加新路由时容易忘。 - 正则顺序重要。 第一个匹配赢,不是最具体那个。重排正则块会改变行为。保持纪律:"正则块放底部,按优先级排"。
^~完全跳过正则。 有location ^~ /static/与location ~ \.html$时,请求/static/page.html会去静态处理,不去 HTML 处理。常常是想要的;偶尔出乎意料。
诊断"哪个 location 处理了我的请求?"的命令:在每个候选里加 add_header X-Loc "matched-here" always;,发请求看响应头。省你几小时。
proxy_pass 与那个能毁了你一天的尾斜杠
location /api/ {
proxy_pass http://backend/; # 一个斜杠
}
location /api/ {
proxy_pass http://backend; # 没斜杠
}
它们不一样。请求 /api/users:
- 用
http://backend/(带尾斜杠):nginx 把匹配的前缀/api/去掉,转发/users给后端。 - 用
http://backend(无尾斜杠):nginx 把完整原始 URI/api/users转发给后端。
有人为这一条调试半天。几乎总是想要的"挂载路径"模式:
location /api/ {
proxy_pass http://backend/; # 双尾斜杠 → 路径重写
}
如果你需要逐字保留路径(透明反向代理):
location /api/ {
proxy_pass http://backend; # 无尾斜杠 → 透传
}
正则 location 不能用重写形式——location 是正则时,带 URI 的 proxy_pass 会被拒。这种情况用 rewrite 显式构造上游路径。
alias vs root:选对那个
两条指令都把 URL 映射到文件系统路径,行为不同。
location /static/ {
root /var/www;
}
# 请求 /static/style.css → /var/www/static/style.css
location /static/ {
alias /var/www/assets/;
}
# 请求 /static/style.css → /var/www/assets/style.css
root 追加 URL 到配置路径。alias 替换匹配的前缀为配置路径。
陷阱:alias 忘了尾斜杠。没尾斜杠路径会怪异地拼接:
location /static/ {
alias /var/www/assets; # 无尾斜杠
}
# 请求 /static/style.css → /var/www/assetsstyle.css ← 坏了
规则:alias 用于目录形式 location 时,location 以 / 结尾、alias 路径也以 / 结尾。不匹配就是 bug。
更宽泛的规则:优先 root,除非确实需要路径替换才用 alias。root 意外更少。
try_files:能找到静态就服务,否则降级
SPA 常见模式:
location / {
try_files $uri $uri/ /index.html;
}
try_files 按顺序检查每个参数,服务第一个存在的。最后一个参数是内部的——作为 fallback URI 使用,不会重新跑 location 匹配,避免循环。
Phoenix 风格的 "/api/* 走后端、其他都是静态 SPA" 应用:
server {
root /var/www/spa;
location /api/ {
proxy_pass http://api/;
}
location / {
try_files $uri $uri/ /index.html;
}
}
这是"静态前端 + 反向代理 API"的标准配置。前缀匹配的 location 块顺序无关,nginx 选最长的。
Forwarded headers:必须设的那些
nginx 反向代理给后端时,后端默认看到的是 nginx 的 IP,不是客户端的。不干预的话:
- 后端日志里所有请求来自
127.0.0.1。 - Geo-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;
后端需要信任这些头。信任必须以"请求来自已知代理"为前提——否则你网络外的攻击者发 X-Forwarded-For: 127.0.0.1 就被当 localhost。
更新的 Forwarded 头(RFC 7239)是规范认可的替代:Forwarded: for=192.0.2.43;proto=https;host=example.com。定义更清楚但支持没那么普遍,许多后端仍期望 X-Forwarded-*。
WebSocket:Upgrade 之舞
朴素 proxy_pass 配置会丢掉 WebSocket upgrade。代理 WebSocket:
location /ws/ {
proxy_pass http://backend/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
proxy_http_version 1.1 是必需的——HTTP/1.0 没有 upgrade 机制。Connection: upgrade 头让 nginx 不要发它默认的 Connection: close。
长连 WebSocket 还需要把 proxy_read_timeout 调高,否则 nginx 默认 60 秒就把空闲连接杀掉。
TLS:真正重要的配置
最低的现代配置(假设有 Let's Encrypt 等):
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
注意:
- 关掉 TLS 1.0 和 1.1。 都有已知问题;2020 年主流浏览器全部弃用。现代 nginx 默认 1.2+,确认一下。
http2 on替换旧的listen 443 ssl http2写法(nginx 1.25 起弃用)。如果能跑 HTTP/3,加listen 443 quic reuseport和add_header Alt-Svc 'h3=":443"; ma=86400';。- HSTS 只用在生产。HSTS 钉死到坏证书的预发环境是数天恢复。
ssl_prefer_server_ciphers off是现代推荐——让客户端从服务器允许列表里挑。"反过来"在 BEAST/CRIME 时代是对的,今天不对。- cipher 列表: Mozilla 的 SSL Config Generator 给当前最佳实践 cipher 字符串。每隔几年更新一次。
HTTP→HTTPS 跳转:
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
用 return 301,不是 rewrite。rewrite 更贵,是用来改路径的,不是做整 URL 跳转。
压缩:gzip 与 brotli
gzip on;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on;
gzip_comp_level 6是实践甜蜜点。再高 CPU 不划算。gzip_types指定要压缩的 content-type。text/html默认开,列表里不写。新字体格式、现代图片格式没在列表里就不压缩出。gzip_vary on设Vary: Accept-Encoding,告诉缓存响应随编码变化。- 不要压已经压缩过的 资产(jpg、png、mp4、woff2)。已压缩数据再压是负收益。
- Brotli(
ngx_brotli模块)在 HTML/CSS/JS 上比 gzip 多压 15–25%,速度相当。如果构建里包含就值得用——多数现代包默认不带。
限流:双桶系统
nginx 有两种限流机制:
limit_req—— 基于漏桶的请求速率限流。按 key(通常客户端 IP)限 RPS。limit_conn—— 并发连接限流。按 key 限同时连接数。
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_conn conn 10;
proxy_pass http://backend/;
}
}
陷阱:在 Cloudflare 这类 CDN 后面,$remote_addr 是 CDN 的 IP,不是客户端的,所有用户都被合到同一个 key 里。用 $http_cf_connecting_ip(Cloudflare 专用),或设 set_real_ip_from 与 real_ip_header X-Forwarded-For。
常见坑
- location 匹配修饰符选错。 用
add_header显式调试。 proxy_passURI 尾斜杠。 两份看起来一样的配置行为不同。alias尾斜杠。 与 location 尾斜杠匹配。- 代理时丢 header。 显式
proxy_set_header Host $host;。 - HTTPS 跳转用
rewrite而不是return。 server_name与请求Host不匹配 会落到默认server块。证书错配常表现为"显示的是别的网站"。if滥用。 nginx 的if在location块里出名地诡异;官方 wiki 那页标题就是 "If is Evil"。能用try_files、map、error_page就用。- 忘了
client_max_body_size。 默认 1 MB。文件上传超就是 413 Payload Too Large。 - 默认
keepalive_timeout75s 在高负载下保留太多空闲连接。按流量调。 - 不检查语法就 reload 配置。 永远先
nginx -t。坏的 reload 杀掉运行中的服务器。 - 改完配置忘了
nginx -s reload。 运行中进程还在用老配置。 access_log off关掉了你事故时想要的日志。用一种简化日志格式而非禁日志。
运维卫生
- 每次配置改动用
nginx -t校验,再 reload。 - reload(不是 restart)是优雅的:
nginx -s reload。 - 日志格式包含
$request_time、$upstream_response_time、$status用于性能调优。 - 用
logrotate+nginx -s reopen(或USR1信号)轮转日志,否则文件会涨爆。 - 如果你需要 TLS 1.3 0-RTT、高级 HTTP/3、主动健康检查,把 nginx 放在别的东西后面——Caddy 与 HAProxy 各自把某些事做得更好。多数负载里 nginx 仍是对的默认。
用 nginx 还是别的
- nginx 用于:TLS 终止、反向代理、静态文件、通用 HTTP 路由。默认选择;稳定、快、文档全。
- Caddy 用于:低配置成本部署。自动 Let's Encrypt、现代 HTTP/3、配置语法更简洁。
- HAProxy 用于:TCP 层负载均衡、高级健康检查、金融级可用。L4/L7 LB 的可观测性比 nginx 强。
- Envoy 用于:service mesh 边车、原生 gRPC 路由、由控制面(Istio、Consul Connect)下发动态配置。
- Traefik 用于:容器化环境,proxy 直接读 Docker / Kubernetes label。
nginx 的配置语言显出了岁月。Caddy 这种现代替代品做同样的事行数减半。但 nginx 装机量巨大、文档详尽、上面那些反直觉行为是稳定的反直觉——你知道了它们不会再变。
2026 年写新 nginx 配置,能省时间的纪律:用一个生成器(本站工具,或一份已知好用的模板),逐步定制,每次改动 nginx -t 后再 reload。这些坑不会被修,唯一的前进路径是知道它们在哪儿。
主要参考资料
用于核对本文技术细节的标准与官方文档。
在线生成干净的 nginx 配置
本站 nginx 工具按你的勾选——TLS、反向代理、gzip、安全头、限流、缓存——生成一份能跑的 server 块,关键的坑(HTTP→HTTPS 跳转、WebSocket upgrade、proxy header)都接对。新站起步或老配置审计时很有用。本地生成,不离开浏览器。
打开 nginx 工具相关文章
继续阅读同一主题领域的实践指南。
Node 生产 Dockerfile 里到底该有什么,不该有什么
网上大多数 Node Dockerfile 都把 node_modules 直接拷进镜像、用 root 运行、最后产出一个 900 MB 的层。本文只讲那几个真正影响构建时间、镜像体积和运行时安全的决定:基础镜像、多阶段构建、依赖层缓存、NODE_ENV 陷阱,以及为什么你的 docker-compose 不该照搬生产。
在用户之前发现缺失的翻译键和插值参数不匹配
缺失的翻译键会把原始键路径直接渲染给用户,插值参数不匹配会渲染出空串或崩溃。这两者在评审中都容易漏,因为开发者的语言包永远有全部键。本文讲如何结构化比较 locale JSON 文件、找出缺失键,并在发布前抓住参数不匹配。
能真正压测 UI 的 mock 数据(而不是只把页面填满)
大多数 mock 数据是同一行复制十遍、只换个 id。它填满页面,却什么都测不到。本文讲如何生成能压测布局边界、长名字、缺失字段、空状态,以及会破坏格式化代码的日期和数字格式的 mock 数据,并通过字段推断让一个 JSON 样本一步变成贴近真实的数据集。