Nginx location配置详解:匹配顺序、常见坑与实战指南
1. Location 是你最容易写错又最影响线上的一行配置大概每一个和线上环境打过交道的人都见过被 location 配置坑到加班的情况。Nginx 的 location 指令看似只是一个“路径匹配”但它牵扯到 root、alias、proxy_pass、try_files 这一整套资源分发逻辑任何一个符号写错轻则接口 404重则整个站点不可用。这篇文章我想把 Nginx location 配置从头到尾梳理一遍不吹概念直接讲清楚匹配顺序、常见场景和一堆我实际踩过的坑。1.1 从一次静态资源 404 开始上个月帮一个团队排查上线事故现象很典型主站首页可以打开CSS 和 JS 全部 403/404控制台刷了一屏红色。我登上服务器打开 Nginx 配置看到这样一段location /static/ { root /data/www/project/dist; }不细看会觉得没问题可实际上root会把完整的 URI 拼到目录后面——请求/static/css/app.cssNginx 找的是/data/www/project/dist/static/css/app.css。如果构建产物里根本没有static这一层那就只能 404。改成alias或者调整目录结构问题立刻消失。这个案例不是个例。location 配置的难度在于它不是一个简单的“URL 等于某个值就执行某段逻辑”而是一套有优先级、有继承、有上下文关联的路由决策机制。你要先理解 location 怎么匹配再理解匹配之后 root、alias、proxy_pass 这些指令到底拿什么路径去工作否则就是“配置能启动一上线就炸”。1.2 location 匹配的是“归一化之后的 URI”不是浏览器地址栏原文很多人忽略了一个事实Nginx 在对请求做 location 匹配之前会先对 URI 做一次归一化处理。浏览器地址栏里的//api//user、/a/../b、%2f这类写法到了 location 匹配阶段已经不是原样了。具体来说Nginx 会剥离 query string所以location匹配时不需要考虑?后面的参数合并连续的多个/比如/foo//bar会归一化为/foo/bar解析.和..比如/foo/../bar会归一化为/bar对%XX形式的 URL 编码做解码个别特殊场景下还涉及编码展开的细节。这意味着在配置文件里写location /foo/../bar基本没意义因为请求进来后 URI 早就变成/bar了。你要是真想让某个路径规则生效应该直接写归一化后的形式。这个特性和$uri变量也有关。location 匹配用的是归一化后的$uri而$request_uri才是浏览器传来的原始 URI。做跳转、记录日志、生成签名链接时这俩变量经常把人绕晕。比如location /old/ { return 301 /new/$request_uri; }如果按$request_uri拼会保留原始双斜杠或编码有些场景下这不是你想要的。我一般在重写规则里优先使用$uri只有明确要保留原始地址时才用$request_uri。1.3 什么时候该把配置写进 locationlocation 不是万能的条件容器。很多人会把变量判断、环境判断、甚至复杂 if 逻辑都塞进 location结果踩中 Nginx 的“if 陷阱”。官方文档里那句话我一直记得if在 location 里只有使用return和rewrite时才是稳妥的其他用法都可能产生不可预期的行为。一个合理的判断标准是这个配置是否需要根据 URI 不同而不同如果是放进 location如果是根据 header、host、客户端 IP 之类的条件优先考虑在 server 层用map或单独 server 块做判断别硬塞到 location 里堆 if。后面在第五章我会专门讲工程化整理。2. 五种匹配修饰符的优先级我用一条判定链给你讲透2.1 修饰符一览无前缀、、^~、~、~*location 支持五种写法每种语义完全不同。我先放一张对照表接下来再逐个解释。写法修饰符作用示例location /api/无普通前缀匹配先记下最长前缀后续还可能被正则覆盖location /api/ { ... }location /api精确匹配URI 必须完全相等命中立即结束location /api { ... }location ^~ /static/^~前缀匹配命中后不再检查正则location ^~ /static/ { ... }location ~ \.php$~正则匹配区分大小写按配置顺序检查location ~ \.php$ { ... }location ~* \.jpg$~*正则匹配忽略大小写按配置顺序检查location ~* \.jpg$ { ... }location fallback命名 location只能被内部指令跳转不能处理外部请求location fallback { ... }初学者最容易混淆的是普通前缀匹配和正则匹配。普通前缀只看“以某字符串开头”它不会管这个字符串后面是什么正则匹配则是完整正则表达式匹配 URI。两者共存时不是简单谁写前面谁生效Nginx 有一套独立的裁决顺序。2.2 Nginx 的真正匹配顺序先最长前缀再按顺序扫正则我按 Nginx 源码行为和官方文档把匹配顺序拆成了这样一条判定链记熟就不会踩坑先查精确匹配。如果有并且 URI 完全相等立刻命中结束匹配没有精确匹配时把所有普通前缀和^~前缀都拿出来做最长前缀匹配记住匹配结果如果第 2 步选出的最长前缀带^~直接采用跳过后续正则检查如果第 2 步选出的最长前缀是普通前缀则按配置文件里正则出现的顺序依次检查~和~*第一个匹配到哪个正则就采用哪个如果没有正则匹配或者没有写正则就使用第 2 步的最长前缀结果。这里有个非常反直觉的点正则的优先级不是“写在 location 前面就高”而是“前缀先选出最长再由正则覆盖”。正则之间才是按顺序执行的不是按写的位置自动获得优先级。我也见过网上有人总结成“无修饰符~^~”这种说法不够准确。准确的说法是最高精确匹配独立于前缀链路^~只是避免了正则覆盖并不天然高于其他前缀匹配普通前缀之间由最长匹配决定和配置文件顺序无关正则之间由配置文件顺序决定和正则长短、精确度无关。2.3 用具体请求走一遍判定流程直接看一段实际配置然后带几个请求走一遍server { listen 80; server_name example.com; location / { try_files $uri $uri/ /index.html; } location /favicon.ico { access_log off; log_not_found off; } location ^~ /static/ { alias /data/www/static/; expires 30d; } location ~* \.(js|css|png|jpg|jpeg|gif|svg|webp)$ { expires 7d; add_header Cache-Control public; } location /api/ { proxy_pass http://backend; } }请求/static/js/main.js没有精确匹配命中前缀匹配候选有/和/static/最长前缀是/static//static/带^~直接命中后面的静态资源正则不会执行。请求/user/profile没有精确匹配最长前缀是/按顺序扫正则~* \.(js|css|...)$不匹配结果落到location /执行try_files。请求/api/login没有最长前缀是/api/正则不匹配虽然/api/这个 URI 不以 js、css 等结尾结果落到/api/走反向代理。请求/logo.png没有最长前缀是/正则~* \.(png)$匹配所以正则 location 覆盖了普通前缀命中缓存规则。如果这时候你想要“让/api/下的图片也走代理而不是走静态缓存”答案很简单把/api/也改成^~比如location ^~ /api/ { proxy_pass http://backend; }这样最长前缀命中后跳过正则。很多人搞不清这个原因就会在正则里写大量排除条件搞得配置又长又难维护。2.4 尾部斜杠带来的路径边界问题斜杠是 location 配置里的隐形杀手差一个斜杠匹配范围完全不同。location /api匹配/api、/api/、/api/v1、/apic。它只做前四个字符的匹配/apic也会命中这个范围往往比你预期的大location /api/匹配/api/、/api/v1但不匹配/apilocation /api/只匹配/api//api不匹配location /api只匹配/api/api/不匹配。所以设计接口路由时我通常默认写location /api/加尾斜杠避免把/apixxx这类路径误匹配进 API。静态资源也是同理location /static/比location /static更精确。你需要“从某个前缀开始的所有请求”时用尾斜杠需要“精确只命中一个 URI”时用。如果非要匹配“路径边界”正则是个选择location ~ ^/api(/|$) { proxy_pass http://backend; }^/api(/|$)表示/api后面必须跟着/或者是字符串末尾这样/apic就不会命中。但能用前缀表达清楚的场景我还是优先前缀少用正则性能更好。3. 静态站点、SPA 与反向代理三个高频场景的 location 实战写法3.1 纯静态站点root、try_files 和缓存头的搭配纯静态站点是所有场景里最简单的但也最容易把 root 和别名搞混。我先给一个推荐配置再解释为什么这么写server { listen 80; server_name www.example.com; root /data/www/example; index index.html; location /favicon.ico { access_log off; log_not_found off; } location /robots.txt { access_log off; log_not_found off; } location / { try_files $uri $uri/ 404; } location ^~ /assets/ { expires 30d; add_header Cache-Control public, immutable; access_log off; } }root写在 server 层后location 内部没有特殊情况时就不需要重复写Nginx 会继承。try_files $uri $uri/ 404的意思是先看看有没有对应文件再看有没有对应目录都没有就返回 404。这里用404而不是跳转到自定义错误页是为了避免静态文件缺失时还跑一堆额外逻辑。location ^~ /assets/的^~在这里很重要。它确保 assets 下的图片、字体、样式请求不会掉进正则 location直接命中并添加缓存头。我在给静态站点做优化时特别喜欢用^~ /assets/这种写法语义非常简单这类资源不做正则判断直接按静态文件处理。3.2 SPA History 路由try_files 回退 index.html单页应用SPA是 location 配置里另一个高频场景。前端路由是 History 模式时浏览器地址栏的/user/123在服务器上没有对应文件如果 Nginx 找不到文件就返回 404那用户一刷新页面就没了。正确的做法是让未命中的路径回退到index.html由前端路由接管location / { root /data/www/spa; index index.html; try_files $uri $uri/ /index.html; }这里的第三个参数/index.html是内部跳转地址请求/user/123找不到文件时Nginx 会内部重写到/index.html然后返回前端入口。注意这个路径是相对于 root 的不是完整 URI。这里有几件事要同时处理好API 请求必须从 SPA 回退中分离出来。你至少要有location /api/ { proxy_pass http://backend; }这样的配置否则/api/user这类请求找不到文件时也会被回退到index.html前端拿到一坨 HTML然后报 JSON 解析失败静态资源也应该和 SPA 回退分离否则像logo.png这种文件存在但路径拼错时不会返回 404反而会返回index.html排查起来特别痛苦。我习惯把构建产物里的 assets 单独拎出来location ^~ /assets/ { root /data/www/spa; expires 30d; add_header Cache-Control public, immutable; }如果你用 Vite 或 Webpack 构建产物里带有 hash 的文件很适合长期缓存但index.html本身不能缓存太久否则版本更新后用户还是旧页面。一般我会在location /index.html里设置no-cache。3.3 反向代理proxy_pass 的斜杠语义与常见误区反向代理里的 location 是坑最多的。尤其是proxy_pass后面到底加不加斜杠很多工作三五年的人也会弄错。先记一条规则proxy_pass的 URL 分为带 URI 和不带 URI 两种情况。不带 URI 时请求 URI 原样传给后端location /api/ { proxy_pass http://backend; }请求/api/login后端收到的还是/api/login。带 URI 时Nginx 会用proxy_pass里的路径替换掉 location 匹配到的那一段然后拼接剩余部分location /api/ { proxy_pass http://backend/; }请求/api/login后端收到的是/login。因为/api/被换成了/。再看一个更细的例子location /api/ { proxy_pass http://backend/v2/; }请求/api/user后端收到/v2/user。同理请求/api/user/profile后端收到/v2/user/profile。如果 location 不带尾斜杠比如location /api { proxy_pass http://backend/api; }请求/api/user匹配的部分是/api剩余是/user拼接后是/api/user请求/apidoc匹配/api剩余doc拼接后变成/apidoc后端收到/apidoc——这绝对不是你想要的结果。我的个人习惯是后端接口本身就是完整路径时proxy_pass http://backend;和location /api/配合原样转发最不容易出错。只有当后端路径和前端路径不一样比如前端叫/api/后端叫/v1/或者需要去掉版本前缀时才用带 URI 的写法。改完一定要用 curl 打一下真实请求确认后端收到的 path 符合预期。WebSocket 场景下也有同样的斜杠问题。举例location /ws/ { proxy_pass http://ws_backend/; }前端连接/ws/chat后端收到的路径变成了/chat如果后端 WebSocket 路由注册在/ws/chat自然连不上。这时候把proxy_pass改成不带 URI 的形式就正常了location /ws/ { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }3.4 命名 location 的用途与限制命名 location 是容易被忽略的一种类型它不参与外部请求匹配只能被try_files、error_page、rewrite这类内部指令跳转。优点是语义明确不会暴露成独立 URL。典型用法是统一错误页location / { try_files $uri $uri/ fallback; } location fallback { return 404; }或者配合error_pagelocation maintenance { return 503; } location / { try_files $uri $uri/ maintenance; }命名 location 的局限在于它不接受外部访问你没法直接在浏览器输入example.com/fallback看到它。内部跳转时它会占一次内部 rewrite不过 Nginx 对内部跳转的效率还是很高的不用太担心性能。4. 线上排查实录404、502 和缓存失效背后的 location 问题4.1 root 与 alias 混用导致的路径拼接事故先从最常见的 404 说起。root和alias的区别就是一个做加法一个做替换。root的拼接规则是root路径 完整 URI。location /static/ { root /data/www; }请求/static/css/app.css实际查找路径是/data/www/static/css/app.css。alias的拼接规则是alias路径 location 匹配后剩余的部分。location /static/ { alias /data/www/public/static/; }请求/static/css/app.css实际查找路径是/data/www/public/static/css/app.css。很多人以为location /static/ { root /data/www; }会去找/data/www/static/这个理解没错前提是项目目录结构里确实存在/static/这一层。但如果项目构建产物没有这层目录那就直接 404。我在实际项目里见过两种常见修法用alias把/static/映射到实际的静态资源目录调整目录结构让文件放在root能拼出来的位置上。哪种更好取决于你的部署方式。如果是打包工具产物我通常建议直接用alias因为构建出的目录往往不是按 URL 前缀组织的。比如项目构建输出在dist/assets但线上 URL 想保持/static/你当然可以用location /static/ { alias /data/www/project/dist/assets/; }只要把 URL 前缀和物理路径解耦后面改目录结构就不会牵一发动全身。4.2 SPA 刷新 404try_files 没写好第二种高频 404 是 SPA 刷新导致。现象是从首页点进去一切正常一刷新/list/123就 404。原因就是配置里只写了 root 和 index没有 try_files 回退location / { root /data/www/spa; }请求/list/123时Nginx 去找/data/www/spa/list/123当然找不到。这时候要么返回 404要么如果开了index可能会去尝试目录下的index.html又失败。修复方案location / { root /data/www/spa; index index.html; try_files $uri $uri/ /index.html; }try_files的执行逻辑是第一个参数找不到就试第二个第二个找不到就把请求内部重写到第三个参数。/index.html是内部重写不是浏览器重定向所以浏览器地址栏保持不变。但这里有个反过来的坑如果某个静态资源真的丢了try_files也会把它重写到/index.html前端拿到的是一份 HTML但响应码是 200。排查这类问题时你会看到“资源加载失败”或者“Unexpected token”很容易让人误判为代码问题。所以我推荐给静态资源单独加一个 locationlocation ^~ /assets/ { root /data/www/spa; try_files $uri 404; }这样构建资源缺失会直接 404不会混进 SPA 回退。4.3 proxy_pass 路径被“吃掉”的问题第三种常见故障是反向代理后接口路径不对表现为后端日志里的请求 path 少了一段或多了前缀。例如location /api/ { proxy_pass http://backend/; }前端请求/api/user后端收到的是/user。如果后端接口本来定义的是/api/user那就会 404。还有更隐蔽的location /api { proxy_pass http://backend/api; }前端请求/apidoc经 location 匹配后剩余部分是doc拼上/api后变成/apidoc。如果你的本意是希望所有/api开头请求都落到后端/api前缀下结果/apidoc也会被代理进去。我的排查方法很简单打开后端访问日志看真实的$request_uri是什么。如果发现路径变了先检查 proxy_pass 的 URL 是否带 URI。不带 URI 就是原样转发带 URI 就会替换匹配段。如果你和我一样经常在多个微服务之间做转发推荐用 upstream 把后端地址管理起来upstream api_upstream { server 10.0.0.2:8080; keepalive 32; } server { location /api/ { proxy_pass http://api_upstream; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }后面机器扩容、端口调整只需要改 upstreamlocation 里的转发逻辑完全不用动。4.4 location 与 add_header 的继承“陷阱”这个坑比较隐晦一般要等到安全扫描或者对比响应头的时候才会发现。Nginx 的add_header继承规则是如果当前配置块比如 location里定义了任何add_header那么外层配置块里的add_header全部失效如果当前块没定义才继承外层。举个例子server { add_header X-Frame-Options SAMEORIGIN; location /api/ { add_header Cache-Control no-store; } }这段配置里访问/api/时响应头只有Cache-Control没有X-Frame-Options。安全扫描一跑直接爆一个“缺少 X-Frame-Options”问题。解决方法是把需要并存的 header 都写在同一层server { location /api/ { add_header Cache-Control no-store; add_header X-Frame-Options SAMEORIGIN; } }如果你用新版 Nginx可以确认一下add_header的inherit参数是否可用但我的经验是别依赖这个特性因为线上不同服务器版本不一定统一。最稳妥的做法要么别在子块写 add_header要么把该有的头都写全。4.5 内嵌 location 与 if 的隐藏风险Nginx 支持 location 嵌套但这个功能大部分人用不好我也不推荐日常使用。原因在于 location 嵌套并不是“路径拼接”而是“父 location 命中后再一次重新匹配子 location”。比如location /user/ { location /posts/ { proxy_pass http://posts_backend; } }你以为是/user/123/posts/匹配实际上请求/posts/today也会命中内嵌 location因为父 location 只是把/user/当作普通前缀一旦转进来内部又做了一次 location 匹配和/user/这段路径已经完全无关。这种配置很容易让人产生路径拼接的错觉出问题后极难定位。location 里的if更危险。官方文档“If is Evil”不是段子if在 location 里经常引发段错误或者多段继承问题。尤其是这样的写法location / { if ($request_filename ~* \.html$) { expires 7d; } }它会让你在语义上产生“满足条件就应用配置”的错觉但 Nginx 的配置是声明式的不是命令式 if 逻辑。我的建议是能用try_files、map、server拆分解决的绝对不用 location if。5. 工程化建议location 配置的规范化与性能优化5.1 先列目录再写 location我看到太多配置文件把 location 写得很随意正则和前缀混在一起顺序也没有层次。时间一长没人敢动那台服务器的配置因为一动就不知道会踩到哪个正则。我自己的习惯是先列一份“路由目录”按业务逻辑分类再整理 location精确匹配favicon、healthz、robots.txt静态资源assets、static、uploadsAPI 反向代理/api/、/admin/SPA 回退/错误页fallback、maintenance。于是配置自然长成这样server { listen 80; server_name example.com; root /data/www/example; index index.html; location /favicon.ico { access_log off; log_not_found off; } location /healthz { access_log off; return 200 ok; } location ^~ /assets/ { expires 30d; add_header Cache-Control public, immutable; access_log off; } location /api/ { include proxy_params; proxy_pass http://api_upstream; } location / { try_files $uri $uri/ /index.html; } }这样谁接手都能一眼看懂精确匹配放在最上面静态资源单独拦截API 单独转发剩下的交给前端路由。每条规则之间不会互相干扰。5.2 正则能少用就少用正则匹配在高并发下确实有性能损耗虽然 Nginx 内部对正则做了缓存但每个新请求第一次匹配时还是要走 PCRE。如果你在国外或国内高流量站点待过应该见过pcre_jit on;开启后正则性能翻倍的效果。但就算有 JIT复杂的正则也远不如前缀匹配快。我给自己定了一条规矩能写前缀匹配的就不写正则必须写正则时把它们集中放在一个区域按业务顺序排列并且用注释注明匹配意图。比如静态资源和带版本号的路由能用^~就用^~location ^~ /static/ { alias /data/www/static/; expires 30d; } location ^~ /api/ { proxy_pass http://backend; }如果你编译的 Nginx 支持开启 JITpcre_jit on;但注意这是全局配置放在http块里。有的老版本编译参数没带 PCRE JIT直接写也不会生效可以通过nginx -V查编译参数。5.3 用 nginx -T 和 curl 做配置核验改完 location别急着 reload。我每次都会先跑两遍nginx -t nginx -T | grep -n locationnginx -t只做语法检查不会告诉你匹配逻辑对不对。要确认实际效果还要用 curl 打一组代表性 URLcurl -I http://127.0.0.1/static/css/app.css curl -I http://127.0.0.1/api/user curl -I http://127.0.0.1/user/profile-I发的是 HEAD 请求Nginx 默认 fastcgi 等模块也可能不支持 HEAD但静态文件、location 匹配、响应头基本都能验证到。观察三样东西HTTP 状态码、Content-Type、Cache-Control。状态码不对说明匹配链有问题Content-Type 不对说明 root/alias 拼错Cache-Control 不对说明 add_header 覆盖或缓存配置没生效。如果还是查不出问题在测试站点临时开 debug 日志error_log /var/log/nginx/debug.log debug;开启后请求一次日志里会出现 location 匹配过程的详细记录。注意线上别长期开着 debug流量大的话日志量很吓人。5.4 遇到复杂路由先拆服务别硬塞在 location 里最后一个建议可能不太像技术但很实用。当你的 location 开始需要大量if、连续rewrite、变量判断、多个 proxy_pass 分支时不要继续在 Nginx 配置文件里堆逻辑了。Nginx 的强项是静态处理和反向代理不是业务逻辑引擎。我看到过的“终结级”复杂配置长这样一个 location 里写了三个 if每个 if 里又嵌 rewriterewrite 的目标还是另一个 location另一个 location 里又根据 host 环境变量选不同的 upstream。这种配置维护成本极高任何一个改动都可能牵动整个链路。更好的做法是业务检测、鉴权逻辑放到后端服务或网关层需要条件路由时用map在 server 层预计算变量而不是在 location 里堆 if确实需要脚本化处理可以考虑 OpenResty 这类带 Lua 能力的版本用代码管理逻辑用 Nginx 管理流量。我自己现在遇到复杂路由需求第一反应是先问一句“这段逻辑能不能拆成一个单独服务”如果能就别为难 Nginx 配置文件。毕竟 location 最理想的状态是一眼能看懂它负责什么而不是变成另一个需要 debug 的系统。