Skip to content

Nginx 配置说明

这篇文章解决什么问题

很多人会用 Nginx,但真正出问题时,常常不是不会写一段能跑的配置,而是不清楚这些问题:

  • 请求到底是怎么命中某个 serverlocation
  • rootaliasindextry_files 到底是什么关系
  • 为什么 proxy_pass 多一个斜杠,后端收到的路径就变了
  • 为什么 add_header 在子层里写了一次,上层头部就没了
  • 为什么代理链后面拿到的客户端 IP 不对
  • 为什么看起来只是一个反向代理,结果要同时处理超时、缓存、Cookie、重定向、压缩和日志

这篇文章不只讲“怎么跑起来”,而是把日常最常用、也最容易配错的配置点放到同一张图里。

先说结论

  • Nginx 配置最重要的不是单个指令,而是层级和匹配顺序。先把 httpserverlocationupstreammap 的关系理清,再写配置才不容易乱。
  • 大多数线上问题,不是出在“不会写”,而是出在五个地方:location 选错、root/alias 混用、proxy_pass 路径替换搞错、转发头不完整、代理链里的真实 IP 没处理好。
  • 对静态文件,优先想清楚 rootindextry_files。对反向代理,优先想清楚四件事:路径怎么转、头怎么传、超时怎么设、缓冲怎么开
  • 能用 return 的重定向,不要先上 rewrite。能用 map 的条件分流,不要把复杂判断堆进 if
  • HTTP 代理和 TCP/UDP 代理不是一套配置。前者主要在 http 下,后者主要在 stream 下。

先把配置结构看全

Nginx 配置不是平铺的,它是分层的。最常见的结构是这样:

nginx
worker_processes auto;

events {
  worker_connections 1024;
}

http {
  upstream backend {
    server 127.0.0.1:3000;
  }

  map $http_host $is_admin {
    default 0;
    admin.example.com 1;
  }

  server {
    listen 80;
    server_name example.com;

    location / {
    }
  }
}

stream {
  server {
    listen 3306;
  }
}

可以把它理解成这样:

层级作用
main全局层,放进程级配置,比如 worker_processes
events连接处理方式,比如 worker_connections
httpHTTP 站点总容器,大部分 Web 配置都在这里
upstream上游服务组,给代理或负载分发用
map根据变量算出另一个变量,适合做条件映射
server一个虚拟站点,按端口和域名接请求
location站点内部按路径分流
streamTCP/UDP 代理,不走 HTTP 语义

如果只记一条:

  • server 决定“这个请求进哪个站”
  • location 决定“这个路径怎么处理”

请求是怎么命中配置的

先选 server

Nginx 先看请求到达的地址和端口,再结合 Host 头找对应的 server_name

最常见的规则是:

  1. 先按 listen 匹配地址和端口。
  2. 再按 server_name 匹配域名。
  3. 如果没匹配到,就走这个端口上的默认站点。
  4. 如果没有显式 default_server,通常就是这个端口上的第一个 server
nginx
server {
  listen 80;
  server_name example.com www.example.com;
}

server {
  listen 80 default_server;
  server_name _;
  return 444;
}

这种“兜底站点”很有用,能避免未定义域名落到错误站点。

再选 location

location 的优先级不是“谁写前面谁先中”,而是按匹配规则来的。

写法含义优先级特点
location = /精确匹配最高,命中就结束
location /path/前缀匹配先找最长前缀
location ^~ /images/前缀匹配且禁止再测正则前缀命中后不再看正则
location ~ \.php$正则匹配,区分大小写在前缀匹配后按出现顺序测试
`location ~* .(jpgpng)$`正则匹配,不区分大小写
location @fallback命名位置只用于内部跳转,不直接接普通请求

核心规则是:

  • 先找最长前缀匹配
  • 再按配置顺序测试正则
  • 如果某个最长前缀带了 ^~,就不再看正则
  • 如果没有正则命中,就回到刚才记住的最长前缀

还有两个很容易忽略的点:

  • location 匹配的是 URI 部分,不含查询参数
  • 匹配前会先对 URI 做规范化处理,比如解码 %XX、处理 ...

末尾斜杠会影响行为

如果一个前缀 location/ 结尾,并且里面是 proxy_passfastcgi_pass 这类转发,访问不带斜杠的同名路径时,Nginx 可能直接回 301,把它补成带斜杠的路径。

nginx
location /user/ {
  proxy_pass http://user.example.com;
}

这时访问 /user,很可能会被重定向到 /user/
如果你不想要这个行为,要显式补一个精确匹配:

nginx
location = /user {
  proxy_pass http://login.example.com;
}

location /user/ {
  proxy_pass http://user.example.com;
}

文件服务相关配置

rootalias 不是一回事

这是最常见的误区之一。

root

root 是把请求 URI 直接拼到目录后面。

nginx
location /i/ {
  root /data/w3;
}

请求 /i/top.gif 时,实际文件路径是:

text
/data/w3/i/top.gif

alias

alias 是用一个目录去替换当前 location 对应的这段前缀。

nginx
location /i/ {
  alias /data/w3/images/;
}

请求 /i/top.gif 时,实际文件路径是:

text
/data/w3/images/top.gif

所以判断很简单:

  • 目录后面还要保留请求里的前缀,用 root
  • 目录就是这段前缀的替代品,用 alias

正则 location 里如果使用 alias,应该配合捕获组,不要直接硬拼。

nginx
location ~ ^/users/(.+\.(?:gif|jpe?g|png))$ {
  alias /data/w3/images/$1;
}

index 不是简单的“默认首页”

index 的作用是处理以 / 结尾的请求,比如 /docs/
它会按顺序检查文件,并触发一次内部跳转

nginx
location / {
  index index.html index.php;
}

这里容易误判的一点是:
index 命中后,请求可能转成 /index.html,然后再被别的 location 接管。

nginx
location = / {
  index index.html;
}

location / {
  # 这里可能才是最终处理 /index.html 的地方
}

所以首页“明明在 location = / 里配了,结果逻辑跑到别处去”,很多时候不是错觉,而是 index 的内部跳转在生效。

try_files 是静态站点和 SPA 的核心

try_files 会按顺序检查文件是否存在,找到就用,找不到就走最后一个后备目标。

普通静态站点

nginx
location / {
  try_files $uri $uri/ =404;
}

这表示:

  • 先找当前路径对应的文件
  • 再找目录
  • 都没有就回 404

前端单页应用

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

这表示:

  • 能命中真实静态文件就直接返回
  • 否则回退到 index.html

这类写法适合 Vue、React 这种前端路由。

try_files 和命名位置

官方文档给出的等价关系很重要:

nginx
location / {
  try_files $uri $uri/ @app;
}

location @app {
  proxy_pass http://backend;
}

本质上接近于:

nginx
location / {
  error_page 404 = @app;
  log_not_found off;
}

所以它不是“语法糖”,而是一种很常用的静态优先,失败再转应用的控制方式。

autoindex 只适合明确要暴露目录列表的场景

nginx
location /downloads/ {
  autoindex on;
  autoindex_format html;
}

它会在找不到 index 文件时输出目录列表。
这适合下载目录、内部文件浏览,不适合默认开在公共静态目录上。

error_page 不只是错误页美化

error_page 可以把错误码转到一个静态页,也可以转到命名位置做兜底处理。

nginx
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;

或者:

nginx
location / {
  error_page 404 = @fallback;
}

location @fallback {
  proxy_pass http://backend;
}

如果只是简单跳转或直接返回状态码,优先用 return
如果要改 URI 并重新进入匹配流程,再考虑 rewrite

反向代理配置

一定先看懂 proxy_pass

proxy_pass 最容易出错的地方,不是会不会写,而是URI 怎么传给上游

1. proxy_pass 不带 URI

nginx
location /api/ {
  proxy_pass http://backend;
}

请求:

text
/api/users

上游通常收到:

text
/api/users

2. proxy_pass 带 URI

nginx
location /api/ {
  proxy_pass http://backend/;
}

同样请求:

text
/api/users

上游通常收到:

text
/users

因为匹配到的 /api/ 被替换成了 proxy_pass 里的 /

3. 正则 location 里,不要随便给 proxy_pass 再拼 URI

官方文档明确提到:
如果 location 是正则,或者是命名位置,Nginx 无法可靠判断应该替换 URI 的哪一段,这时 proxy_pass 应该不带 URI

nginx
location ~ ^/api/(.*)$ {
  proxy_pass http://backend;
}

4. rewrite break 后,proxy_pass 的 URI 处理也会变

nginx
location /name/ {
  rewrite /name/([^/]+) /users?name=$1 break;
  proxy_pass http://127.0.0.1;
}

这时传给上游的是修改后的完整 URI。
所以只要配置里同时出现 rewriteproxy_pass,就不要只盯着一行看,要把两者放一起读。

代理请求时,头部要主动想清楚

一个够用的反向代理模板通常至少要把这些头传对:

nginx
location / {
  proxy_pass http://backend;
  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;
}

为什么这些头重要:

  • Host:很多应用按域名分租户、做回调、生成绝对 URL
  • X-Real-IP:应用想看到客户端来源地址
  • X-Forwarded-For:保留整条代理链
  • X-Forwarded-Proto:告诉后端原请求是 http 还是 https

还有一个容易忽略的事实:
如果你不显式写 proxy_set_header,Nginx 并不会把原始 HostConnection 原样传给上游,而是会按自己的默认值处理。

WebSocket 需要额外配置

nginx
location /ws/ {
  proxy_pass http://backend;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_set_header Host $host;
}

没有这两行:

  • Upgrade
  • Connection "upgrade"

很多 WebSocket 服务会直接失败。

三种超时要分开看

指令控制什么
proxy_connect_timeout和上游建立连接要等多久
proxy_send_timeout向上游发送请求时,两次写操作之间最多等多久
proxy_read_timeout从上游读取响应时,两次读操作之间最多等多久

很多人把 504 都理解成“后端慢”。
实际上有可能是:

  • 连不上上游
  • 请求发不出去
  • 响应长时间没继续返回数据

所以超时要按阶段拆开看。

proxy_bufferingproxy_request_buffering

响应缓冲

nginx
proxy_buffering on;

开启时:

  • Nginx 会尽快把上游响应读进缓冲区
  • 不够时还可能写临时文件
  • 更适合普通页面、接口、缓存场景

关闭时:

  • 响应边到边转发
  • 更接近流式输出
  • 上游慢,客户端也会更直接感受到
nginx
proxy_buffering off;

请求体缓冲

nginx
proxy_request_buffering on;

开启时:

  • Nginx 先把整个请求体读完,再发给上游

关闭时:

  • 请求体收到一部分,就往上游发一部分
  • 适合流式上传,但一旦已经开始往上游发送,请求就不容易再切到下一个上游
nginx
proxy_request_buffering off;

所以大文件上传、流式传输、SSE、长连接接口,通常要主动评估这两个开关,而不是只套一个通用模板。

很多后端服务只知道自己运行在内网地址或子路径下,这时它返回的 LocationSet-Cookie 往往不适合直接发给客户端。

改写重定向地址

nginx
location /one/ {
  proxy_pass http://upstream:port/two/;
  proxy_redirect default;
}

或者显式写:

nginx
proxy_redirect http://localhost:8000/ /;
nginx
proxy_cookie_domain localhost example.org;

把后端返回的:

text
domain=localhost

改成:

text
domain=example.org
nginx
proxy_cookie_path /two/ /;
nginx
proxy_cookie_flags ~ secure httponly samesite=lax;

如果你的站点是“外部访问路径”和“后端真实路径”不一致,Cookie 和重定向改写经常是必须项,不是锦上添花。

动态域名代理时别忘了 resolver

如果 proxy_pass 里用了变量,或者域名是在运行期解析的,就要显式配置 resolver

nginx
resolver 127.0.0.1 valid=30s;

location / {
  proxy_pass http://$target_host$request_uri;
}

如果没有 resolver,这类配置很容易在启动后解析失败或不按预期更新。

upstream 和负载分发

最基础的上游组

nginx
upstream backend {
  server 127.0.0.1:3000 weight=3;
  server 127.0.0.1:3001;
  server 127.0.0.1:3002 backup;
}

server {
  location / {
    proxy_pass http://backend;
  }
}

默认分发策略是加权轮询

  • weight=3:权重更高,分到的请求更多
  • backup:只有主节点不可用时才接流量

健康退避相关参数

nginx
upstream backend {
  server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
  server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
}

这组参数控制的是:

  • 在一定时间内失败多少次,认为这个节点暂时不可用
  • 暂时不可用要持续多久

注意一个关键点:
“什么叫失败”,和 proxy_next_upstream 这类指令有关,不是只有 TCP 断开才算失败。

上游长连接

nginx
upstream backend {
  server 127.0.0.1:3000;
  server 127.0.0.1:3001;
  keepalive 32;
}

location / {
  proxy_pass http://backend;
  proxy_http_version 1.1;
  proxy_set_header Connection "";
}

上游长连接的价值是减少反复建连。
这类配置建议显式写,不要只依赖不同版本的默认值。

什么时候考虑 least_connip_hashhash

  • 请求处理时间差异很大时,可以考虑 least_conn
  • 需要按客户端地址做相对稳定分发时,可以考虑 ip_hash
  • 需要按某个变量做稳定路由时,可以考虑 hash

这几种不是谁更高级,而是谁更贴合你的流量特征。

缓存、压缩和响应头

反向代理缓存

一个最小可用的代理缓存通常分两层:

  1. http 层声明缓存目录和共享内存区
  2. location 层决定哪些请求进缓存、缓存多久、哪些情况绕过
nginx
http {
  proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=page_cache:20m inactive=30m max_size=2g use_temp_path=off;

  server {
    location / {
      proxy_pass http://backend;
      proxy_cache page_cache;
      proxy_cache_key $scheme$proxy_host$uri$is_args$args;
      proxy_cache_valid 200 302 10m;
      proxy_cache_valid 404 1m;
      proxy_cache_lock on;
      proxy_cache_background_update on;
      proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
      proxy_cache_bypass $http_authorization $cookie_session;
      proxy_no_cache $http_authorization $cookie_session;
    }
  }
}

这里最关键的不是记全所有指令,而是理解它们分工:

指令作用
proxy_cache_path定义缓存目录、共享内存区、容量和清理策略
proxy_cache在当前层启用哪个缓存区
proxy_cache_key用什么当缓存键
proxy_cache_valid不同状态码缓存多久
proxy_cache_bypass这次请求不读缓存
proxy_no_cache这次响应不写缓存
proxy_cache_lock同一个新键只让一个请求回源填缓存
proxy_cache_background_update后台刷新过期缓存
proxy_cache_use_stale上游出错时是否继续回旧缓存

几个容易忽略的事实:

  • keys_zone 存的是缓存键和元信息,不是响应体本身
  • inactive 到期会清理长时间没访问的数据,不看它是否“还新鲜”
  • 如果响应带 Set-Cookie,默认通常不会被缓存
  • 如果要让 HEADGET 区分开,缓存键要包含请求方法

静态资源缓存和响应头

nginx
location /assets/ {
  expires 7d;
  add_header Cache-Control "public, max-age=604800, immutable" always;
}

这里要注意两个细节:

  1. add_header 默认不是所有状态码都加,想更稳地覆盖错误页或特殊响应,通常要加 always
  2. 如果你在子层重新写了 add_header,上层的 add_header 不会自动帮你合并

这也是很多人明明在 server 层加了安全头,结果某个 location 里又写了一次 add_header 后,上层头全没了的原因。

gzip 压缩

nginx
gzip on;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
gzip_vary on;

这几项比较关键:

  • gzip:总开关
  • gzip_min_length:太小的响应不压
  • gzip_types:除 text/html 之外,还要压哪些 MIME 类型
  • gzip_vary on:让缓存体系知道压缩是有区分的

另外要记一条风险:

  • 在 TLS 场景下,压缩响应可能受到 BREACH 这类攻击影响

所以涉及敏感反射内容时,不要把“开压缩”当成无脑优化项。

TLS、真实 IP 和访问控制

HTTPS 的稳定写法

nginx
server {
  listen 80;
  server_name example.com www.example.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  server_name example.com www.example.com;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;

  location / {
    proxy_pass http://backend;
  }
}

几个点值得单独记住:

  • 旧时代的 ssl on; 已经过时,直接在 listen 上写 ssl
  • 证书文件里如果要带中间证书,顺序应正确
  • 同一个站点可以同时加载不同类型的证书,比如 RSA 和 ECDSA
  • 如果开 ssl_stapling,还要考虑 resolverssl_trusted_certificate

代理链里的真实客户端 IP

如果 Nginx 前面还有 CDN、SLB、四层代理,直接用 $remote_addr 往往看到的是上一跳,不是真实客户端。

nginx
set_real_ip_from 192.168.1.0/24;
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

这里最关键的不是把功能开起来,而是只信任你真的控制的上游地址

real_ip_recursive on; 的意义是:

  • X-Forwarded-For 链里继续往前找
  • 找到最后一个不在信任名单里的地址,作为真实客户端地址

这类配置如果信任范围写太大,相当于把客户端 IP 的解释权交给了外部请求头。

基于 IP 和密码的访问控制

IP 白名单

nginx
location /internal/ {
  allow 10.0.0.0/8;
  allow 192.168.0.0/16;
  deny all;
}

规则按顺序检查,遇到第一个匹配就结束。

Basic Auth

nginx
location /admin/ {
  auth_basic "closed site";
  auth_basic_user_file /etc/nginx/htpasswd;
}

组合使用

nginx
location /admin/ {
  satisfy any;

  allow 10.0.0.0/8;
  deny all;

  auth_basic "closed site";
  auth_basic_user_file /etc/nginx/htpasswd;
}

satisfy any 表示:

  • IP 白名单通过,或者
  • Basic Auth 通过

满足其一即可。

限流和连接数控制

很多人把“限流”理解成一个开关,但 Nginx 里至少要分两种:

  • limit_req:限制请求处理速率
  • limit_conn:限制并发连接数

这两者不是同一件事。

limit_req:按速率控请求

nginx
http {
  limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;

  server {
    location /login/ {
      limit_req zone=perip burst=10 nodelay;
    }
  }
}

这段配置的意思是:

  • 以客户端 IP 作为键
  • 平均速率限制为每秒 5 个请求
  • 允许短时突发 10 个请求
  • nodelay 表示突发请求不排队,直接按桶容量判断

几个关键点:

  • burst 不是把平均速率改大,而是允许短时超出平均值
  • 不写 nodelay 时,超出平均速率但还没超过 burst 的请求会被延迟
  • limit_req_status 可以改被拒请求的状态码,默认通常是 503
  • limit_req_dry_run on; 可以先观察,不真正拦截

limit_conn:按并发控连接

nginx
http {
  limit_conn_zone $binary_remote_addr zone=addr:10m;

  server {
    location /download/ {
      limit_conn addr 2;
    }
  }
}

这表示:

  • 同一个 IP 同时最多保留 2 个连接

要特别注意官方文档里的一个细节:

  • 在 HTTP/2 和 HTTP/3 里,每个并发请求都按一个独立连接计数

所以如果你把 limit_conn 配得很小,HTTP/2 场景下比你想象中更容易命中限制。

什么时候用哪种

场景更适合
登录、短信验证码、搜索接口防刷limit_req
下载、大文件、长连接接口防占满limit_conn
想先观察影响,不立刻封*_dry_run on

常见误区

  • 只配 limit_conn,却以为已经挡住了高频请求
  • 只配 limit_req,却放任慢连接长时间占资源
  • 直接用 $remote_addr 做键,而前面还有代理层,结果限流对象全错

map 做条件分流,通常比把判断写满 if 更稳

map 的作用是根据一个变量算出另一个变量。

nginx
map $http_host $is_admin {
  default 0;
  admin.example.com 1;
}

或者:

nginx
map $http_user_agent $is_mobile {
  default 0;
  "~Opera Mini" 1;
}

它适合做这些事:

  • 按域名切分站点逻辑
  • 按请求头决定缓存或限流开关
  • 给代理、日志、重定向算派生变量

官方文档还提到一个很关键的特性:

  • map 变量是用到时才计算

所以就算你定义了不少 map,只要没被请求真正用到,不会平白增加每个请求的处理成本。

相比之下,if 更适合简单的返回、限速、少量条件改写,不适合承载整套分流逻辑。

监控、日志和排错

基础访问日志

nginx
log_format main '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent" '
                '$request_time';

access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;

这里最常用的字段里,$request_time 非常有价值,因为它能直观看到请求耗时。

条件记录日志

如果你不想让 2xx、3xx 把日志刷满,可以配条件日志:

nginx
map $status $loggable {
  ~^[23] 0;
  default 1;
}

access_log /var/log/nginx/access.log main if=$loggable;

stub_status

如果只是想快速看连接数和请求总量,可以开一个状态页:

nginx
location = /basic_status {
  stub_status;
  allow 127.0.0.1;
  deny all;
}

它能看到这些基础数据:

  • Active connections
  • accepts
  • handled
  • requests
  • Reading
  • Writing
  • Waiting

要注意:

  • stub_status 模块不是所有构建都默认带
  • 这类页面应该限制来源,不要直接裸露到公网

改配置时的最稳顺序

bash
nginx -t
nginx -s reload

或者系统服务方式:

bash
sudo nginx -t
sudo systemctl reload nginx

如果只是记一个排错顺序,记这个:

  1. nginx -t 是否通过
  2. 看请求是否真的进来了
  3. error_log
  4. 如果是代理,再绕过 Nginx 直接打上游

stream:Nginx 不只会代理 HTTP

很多人只把 Nginx 当 HTTP 反向代理,但它也可以做 TCP/UDP 代理,这部分在 stream 下。

nginx
stream {
  upstream mysql_backend {
    server 10.0.0.11:3306;
    server 10.0.0.12:3306;
  }

  server {
    listen 3306;
    proxy_connect_timeout 1s;
    proxy_timeout 30s;
    proxy_pass mysql_backend;
  }
}

适合这些场景:

  • MySQL
  • Redis
  • 自定义 TCP 服务
  • DNS(UDP)

这套配置和 http 的区别很大:

  • 没有 location
  • 没有 HTTP 头部和 URI
  • 主要按端口、协议、连接层参数处理

另外要注意:

  • stream 模块不是所有构建默认启用

一组更完整的站点示例

下面这份配置把几个常见点放到一起:

nginx
worker_processes auto;

events {
  worker_connections 2048;
}

http {
  proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=page_cache:20m inactive=30m max_size=2g use_temp_path=off;

  map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
  }

  upstream app_backend {
    server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
    keepalive 32;
  }

  server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
  }

  server {
    listen 443 ssl;
    server_name example.com www.example.com;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    root /srv/www/app/dist;
    index index.html;

    access_log /var/log/nginx/example.access.log;
    error_log /var/log/nginx/example.error.log warn;

    location /assets/ {
      expires 7d;
      add_header Cache-Control "public, max-age=604800, immutable" always;
      try_files $uri =404;
    }

    location /api/ {
      proxy_pass http://app_backend/;
      proxy_http_version 1.1;
      proxy_set_header Connection "";
      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_connect_timeout 3s;
      proxy_send_timeout 30s;
      proxy_read_timeout 30s;
      proxy_buffering on;
      proxy_request_buffering on;
    }

    location /ws/ {
      proxy_pass http://app_backend;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;
      proxy_set_header Host $host;
      proxy_read_timeout 60s;
      proxy_buffering off;
    }

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

这份配置表达的是:

  • 80 全跳 HTTPS
  • 静态资源强缓存
  • /api/ 走反向代理
  • /ws/ 走 WebSocket
  • 其他路径走前端 SPA 回退

最容易踩的坑

1. rootalias 混着理解

现象通常是:

  • 文件明明存在,但回 404
  • 路径总是多一层或少一层

处理方式:

  • 先看当前 location 的意图是“拼接路径”还是“替换前缀”

2. proxy_pass 末尾斜杠没想清楚

这是反向代理里最常见的坑。
一多一个 /,上游收到的路径就可能变掉。

3. 正则 location 里还给 proxy_pass 带 URI

这类配置最容易出现路径替换和预期不一致。
正则 location 下通常优先让 proxy_pass 不带 URI。

4. location /foo/ 自动补斜杠

很多人以为是浏览器在跳,其实是 Nginx 的特殊处理。

5. 在子层写了一个 add_header,结果上层头没了

add_header 不是自动叠加的。
只要当前层定义了自己的 add_header,上层的通常不会自动合并进来。

6. 代理链真实 IP 不对,却先去改应用代码

如果前面有 CDN 或负载均衡,先看 set_real_ip_fromreal_ip_headerreal_ip_recursive

7. 动态代理域名没配 resolver

现象通常是:

  • 变量形式的 proxy_pass 解析异常
  • 域名变化后不按预期生效

8. index 触发内部跳转后,被另一个 location 接管

这不是偶发,是 index 的正常行为。

参考链接

基于 VitePress 的个人知识库骨架