
先说一个我印象特别深的排查现场。朋友搭了个内容站静态文件放在/home/blog/www下首页打得开一进文章详情页就 404。他反复调 root、调 location折腾了大半天最后我帮他拉了一下 error.log发现 nginx 实际去找的路径是/home/blog/www/article/article/xxx.html——article被拼了两次。这个nginx 打不开由 root 路径与 uri 拼接起来的路径导致报错的问题说白了不是文件不存在而是你没搞懂 nginx 拼路径的规则以及 location、root、proxy_pass 三者之间到底怎么打配合。我自己的项目里也是被这个坑炸过两三次之后才彻底把路径拼接和转发链路理顺的。这篇文章就围绕这个核心把路径拼接机制、location 匹配优先级、proxy_pass 两种转发形态讲透再给一套可以直接抄走的配置模板。正在搭站点、做前后端分离改造或者在本机/虚拟机里配多端口多站点开发环境的开发者应该都能省下不少排查时间。1. 一个典型的路径拼接型404先看懂nginx到底在找哪个文件1.1 现场还原配置看起来没问题浏览器却打不开这类问题最常见的开局是浏览器地址栏输入http://your-site/article/hello.html页面直接 404但curl http://your-site/首页又是好的。你去看文件系统/home/blog/www/article/hello.html明明存在于是开始怀疑路径权限、怀疑防火墙、怀疑 FastCGI唯独没怀疑 nginx 自己。问题往往出在 location 里又覆盖了一个 root。比如下面这种配置server { listen 80; server_name demo.local; root /home/blog/www; location /article/ { root /home/blog/www/article; } }访问/article/hello.html时nginx 内部会做什么它会用 location 内的root /home/blog/www/article作为基准目录再在后面拼接上完整的请求 URI/article/hello.html。最后落地的磁盘路径就变成了/home/blog/www/article/article/hello.html文件当然不存在于是 404。这个场景我一共见过不下五次每次配置者的第一反应都是我再看看是不是文件权限问题。所以第一步必须建立正确的认知nginx 拼路径不是你想当然的根目录 剩余部分它有自己一套死板的公式找到那条公式问题就已经解决一半了。1.2 root加uri等于最终磁盘路径这条公式必须刻进脑子nginx 官方对 root 的定义其实就一句话root指令设置请求的根目录但它不是你配一个根路径然后 nginx 自动用 location 剩下的部分去找文件。真正的运算过程是最终磁盘路径 root指令的值 规范化处理后的完整请求URI注意这里是完整请求 URI不是去掉 location 匹配前缀之后的剩余 URI。举个例子root /var/www/site;请求/index.html→ 找/var/www/site/index.htmlroot /var/www/site;请求/blog/1.html→ 找/var/www/site/blog/1.html就算你在location /blog/ {}里写了root /var/www/site;请求/blog/1.html→ 依然找/var/www/site/blog/1.html之所以强调第三条是因为很多人会误以为 location 匹配到的/blog/会从 URI 里被摘掉再拼到 root 后面。大多数新手把 root 和 alias 混为一谈根源就在这。nginx 对 URL 还做了一层规范化多斜杠会被压缩、.和..会被解析。所以如果 root 值末尾带了斜杠URI 开头又带斜杠error.log 里可能出现一个看起来很诡异的双斜杠路径例如/var/www/site//blog/1.html。大多数情况下 Linux 文件系统会容忍双斜杠但排错的时候这种路径会把你绕晕我建议写 root 时统一不带结尾斜杠少一个干扰项。1.3 三种常见报错及对应的日志特征路径拼接问题落到 error.log 或浏览器上通常表现为三种先记住它们的特征报错现象error.log 关键字常见原因404 Not Foundopen() /xxx/yyy failed (2: No such file or directory)root/alias 拼出来的路径不对或文件真的不在403 Forbiddendirectory index of /xxx/ is forbidden或Permission denied目录下没有 index 文件或 nginx 工作进程无权限读目录502/504 Bad Gatewayconnect() failed (111: Connection refused)属于 proxy_pass 上游不通跟路径拼接无关但容易混在一起排查看到第一行open() failed的时候不要急着去服务器上创建文件先把你配置里的 root 和请求 URI 手写拼一遍看看拼出来的结果和日志里的路径一不一致。绝大多数打不开都是这一步对不上。2. root与alias的分工追根溯源解决双重路径问题2.1 root是追加alias是替换在 nginx 里root 和 alias 看似都能指定目录语义却完全不同。我习惯用一个特别直白的对比来记root把请求 URI 原样追加到 root 后面alias把 location 匹配到的前缀替换成 alias 的值对着例子看更清楚。假设配置里都有location /img/ {}请求都是/img/logo.png用root /data/files;→ 最终路径/data/files/img/logo.png用alias /data/files;→ 最终路径/data/files/logo.png用alias /data/files/;→ 最终路径同样是/data/files/logo.png看到了吗root 会把/img/原封不动地带进路径alias 则会把请求 URI 里和 location 匹配上的那一段直接换掉。如果你的磁盘目录结构正好是/data/files/img/logo.png用 root 合适如果你的文件系统里根本没有img这个层级文件直接平铺在/data/files/logo.png那就必须用 alias 把/img/这个前缀剥掉。2.2 最典型的双重路径事故复盘回到开头那个案例完整的错误配置是location /article/ { root /home/blog/www/article; }请求/article/hello.html时nginx 做的事情是root 值 /home/blog/www/article完整 URI /article/hello.html拼出/home/blog/www/article/article/hello.html。这个配置的意图显然是我想让 /article/ 这个 URL 访问到 article 目录里的文件但 root 的追加逻辑把article拼了两遍。修复有两种思路对应 root 和 alias 的两种语义方案 A保留 root把 root 降到上一级目录location /article/ { root /home/blog/www; }这样拼出来就是/home/blog/www/article/hello.html正好命中。方案 B改用 alias让 alias 直接顶替掉/article/location /article/ { alias /home/blog/www/article/; }拼出来同样是/home/blog/www/article/hello.html。两个方案在结果上等价但背后逻辑完全不同。我个人的建议是如果你只是希望某个 URL 前缀对应的目录结构不复杂优先用方案 A 的 root因为配置更直观后面也少踩 alias 的边界坑。2.3 选root还是选alias一句判断口诀项目里需要随时做判断的时候我用一句话概括URI 前缀在磁盘上能一一对应用 root对应不上或想把前缀剥掉用 alias。举两个真实场景场景一前端构建产物输出到/srv/www/demo/dist/希望 URLhttps://site/直接访问静态资源全部在这一个目录下。此时直接root /srv/www/demo/dist;即可不要画蛇添足去用 alias。场景二后端把上传图片放在/data/storage/2025/但你想用/uploads/xxx.png来访问而又不想在磁盘里额外建一层uploads目录。这时候就必须location /uploads/ { alias /data/storage/2025/; }请求/uploads/logo.png会落到/data/storage/2025/logo.png磁盘上没有上传目录也能正确访问。如果这里错用了 rootnginx 会去找/data/storage/2025/uploads/logo.png又是一声 404。另外提醒一句alias 对尾斜杠和正则匹配更敏感能用 root 搞定的场景别硬上 alias。alias 在正则 location 里的行为尤其容易出问题我一般只在纯前缀匹配里用它。3. location匹配规则它决定了请求uri接下来归谁处理3.1 五种location写法的优先级排序搞定了文件路径拼接下一个绕不开的就是 location。每次聊到这个我都会先带着把 nginx 的匹配顺序过一遍因为很多人只知道越长的前缀越优先但这只是其中一环。nginx 的完整匹配顺序是这样的先进行精确匹配 /path如果命中直接使用该 location不再继续。否则找出最长的普通前缀匹配如果这个最长前缀是^~修饰的直接采用跳过正则。如果没有^~命中就按配置文件中出现的顺序依次执行正则匹配~区分大小写和~*不区分大小写第一个命中的正则胜出。正则全部没命中才回到第 2 步那个最长普通前缀。如果连普通前缀都没有用兜底的/。把这个顺序拆成一张小表排错时对着看特别方便写法含义命中后的行为 /path精确匹配直接采用最高优先级^~ /path前缀匹配命中后不再看正则直接采用~ /path正则区分大小写按文件顺序第一个命中采用~* /path正则不区分大小写按文件顺序第一个命中采用/path普通前缀匹配记录最长者正则不中才用/兜底前缀以上全不中时兜底有一个特别常见的误区以为 location 写得越靠前越优先。实际上普通前缀匹配之间顺序无关nginx 是按最长匹配取胜只有正则之间有顺序关系谁先出现谁先命中。所以写正则的时候一定要把更具体的正则放前面否则会被宽泛正则抢先。3.2 匹配命中后uri的三条去向location 一旦命中接下来的请求 URI 有三条主流去向这也是理解root 与 proxy_pass 打配合的关键去向一被root或alias映射成磁盘文件。root 用完整 URI 拼接alias 用替换后的部分拼接。去向二被proxy_pass不带 URI原样转发给上游。转发到后端时URI 保持客户端请求的样子location 匹配到的前缀不会去掉。去向三被proxy_pass带 URI做前缀替换后转发。location 匹配到的部分会被 proxy_pass 里的路径整体替换掉这是反向代理里最常用的路径改写手段。另外还有一类内部动作比如rewrite、try_files会生成新的 URI重新走一遍 location 匹配。这个机制很容易被人忽略但它恰恰是路径越配越乱的元凶之一。遇到 try_files 回退到/index.html之类行为时要知道这是二次匹配很多人会误以为明明配了 location /api怎么请求打到前端页面去了。3.3 正则location中使用proxy_pass的限制正则 location 里用 proxy_pass有一个历史悠久的限制nginx 不允许在正则 location 里给 proxy_pass 带静态 URI 部分。换句话说这种写法在nginx -t阶段就会报错location ~ ^/api/ { proxy_pass http://127.0.0.1:8080/; # 报错正则location中不允许带URI }报错信息大致是proxy_pass cannot contain URI part in location given by regular expression。想实现路径改写必须借道变量捕获。最经典的写法是这样location ~ ^/api/(.*)$ { proxy_pass http://127.0.0.1:8080/$1$is_args$args; }这里的(.*)把/api/后面的部分捕获成$1在 proxy_pass 里拼回去同时用$is_args和$args把查询参数也带上。效果等价于去掉/api/前缀的转发。要注意一旦 proxy_pass 里出现了变量nginx 就不再对请求 URI 做额外处理也不能复用一些连接优化性能上会有一点点损耗。小项目基本无感但大规模并发场景下能用普通前缀 location 解决的就别硬上正则。4. proxy_pass的两种形态同样一行location转发结果天差地别4.1 不带URI原样透传proxy_pass 的写法看似简单实质上隐藏着一个很容易被忽略的分水岭代理地址里到底有没有路径部分。先看不带路径的情况location /api/ { proxy_pass http://127.0.0.1:8080; }这种写法里http://127.0.0.1:8080后面没有任何路径nginx 会把请求 URI原样转发给上游。也就是说客户端请求/api/users后端实际收到的是/api/users。很多人在这一行栽跟头是因为直觉上以为我配了 location /api/转发时应该把 /api/ 剥掉了吧——不会的不带 URI 就是透传一个字符都不少。我见过最多的真实案例是前端调/api/login后端接口定义却是/login于是后端一路 404。排查时后端日志里能看到它收到的是/api/login前端和 nginx 都看了个遍就是没人意识到是 proxy_pass 少了斜杠。4.2 带URI替换匹配部分再看带完整 URI 的情况注意区别就在最后多出来的那个/location /api/ { proxy_pass http://127.0.0.1:8080/; }这里出现了/proxy_pass 就有了URI 部分。nginx 的规则是用 proxy_pass 的 URI 部分整体替换掉location 匹配到的前缀。所以客户端请求/api/users后端收到的是/users——/api/被/替换了。如果 proxy_pass 的 URI 更长替换规则同样成立。例如location /api/ { proxy_pass http://127.0.0.1:8080/v1/; }请求/api/users后端收到/v1/users。这一段就是很多人用来换前缀的秘密武器因为它本质上就是一次路径改写却完全不需要写 rewrite。4.3 用代理URI完成接口前缀换血的实战案例说一个我去年处理过的需求最能说明这套逻辑的实用价值。当时有个老后端所有接口都写在/v1/下面/v1/login、/v1/articles。但前端已经按新规范发布了统一请求/api/login、/api/articles。后端项目排期紧张一时半会儿改不了路由前端也不能改压力全给到 nginx。我的处理方式就是在 server 里加一段location /api/ { proxy_pass http://127.0.0.1:8080/v1/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }这样前端/api/login到达后端时已经变成/v1/login后端一行代码不用动。如果不用这个替换特性就得写成location /api/ { rewrite ^/api/(.*)$ /v1/$1 break; proxy_pass http://127.0.0.1:8080; }rewrite 方案也能达到效果但多了一层规则理解成本更高而且 rewrite 会和原本的 proxy_pass URI 机制互相干扰。相比之下直接在 proxy_pass 里写 URI 的方案干净得多。这个案例在我自己的项目里已经稳定跑了快一年没有再为路径匹配操过心。5. 静态文件与接口转发协同作战整套location规划示例5.1 一个前后端分离站点的server配置模板把 root、alias、proxy_pass 全部串起来最典型的就是前后端分离站点的配置。下面这个模板是我个人项目一直在用的你可以直接抄走再改路径server { listen 80; server_name demo.local; charset utf-8; # 前端静态文件走 root兜底交给 index.htmlSPA 路由 location / { root /var/www/demo/dist; index index.html; try_files $uri $uri/ /index.html; } # 图片资源用 alias磁盘目录和 URL 前缀不对称时很管用 location /static/ { alias /var/www/demo/assets/; expires 7d; } # 接口转发去掉 /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; } # favicon 单独精确匹配避免不必要的日志噪音 location /favicon.ico { log_not_found off; access_log off; } }用这个配置模拟几个请求你就能完整看到整套配合逻辑请求/命中location /root 拼接/var/www/demo/dist/index.html正常返回前端首页。请求/static/logo.png最长前缀匹配是/static/alias 把前缀换掉读取/var/www/demo/assets/logo.png。请求/api/users进入/api/proxy_pass 带/转发给后端时已经是/users。请求/favicon.ico精确匹配命中直接走空规则不浪费 access log。这个模板最核心的思路是静态文件用 root/alias 做本地映射动态接口用 proxy_pass 做转发location 前缀作为两者之间的调度开关。谁负责什么一眼就能分清后续加缓存、加限流也都在各自的 location 里改动互不干扰。5.2 多站点开发环境的location注意点现在很多人的开发环境是这样的一台本机或虚拟机跑着 nginx开了多个端口每个站点一个自定义域名通过改/etc/hosts指向127.0.0.1。这种环境下路径拼接问题更容易爆发因为每个 server 块都可能有自己的 root 和 proxy_pass。几个高频翻车点我得单独拎出来说第一别在 http 块里配一个全局 root。全局 root 会被所有 server 继承只要某个 server 忘了配自己的 root就会跑到别人的目录里找文件报错样式千奇百怪。我习惯每个 server 块里明确写 root就算配置长一点也认了。第二server_name 要跟 Host 对齐。开发环境里自定义域名全靠/etc/hosts映射如果你在 nginx 里用server_name demo.local浏览器访问时就必须带上这个 Host。用curl测试时要记得加-H Host: demo.local不然请求会落到默认 server路径和预期完全不是一码事。第三多个 server 之间复制配置时proxy_pass 的端口最容易复制错。比如两个站点一个是 8080 后端一个是 9090 后端从第一个 server 复制一段 location /api/ 到第二个很容易忘了改端口。这种问题日志里看起来就像路径对、服务在但就是连不上。我会在 server 顶部用注释写清楚每个端口对应的服务算是个土办法但相当有效。5.3 排查路径问题三板斧路径相关的问题我有一套固定的排查流程基本上走完三遍都能定位。第一板斧是配置校验改动完先跑nginx -t语法错误、正则 location 带 URI 这类问题在这一步就会现形。第二板斧是拉错误日志。日志位置因发行版而异常见的是/var/log/nginx/error.log改动后重载配置再复现一次请求然后tail -n 100 /var/log/nginx/error.log看到open()那一行把里面的路径和你脑子里的预期路径放在一起比对九成九的问题当场就明白了。第三板斧是实测请求。用 curl 直接看响应和转发结果curl -i http://127.0.0.1:80/api/users -H Host: demo.local如果怀疑是权限问题再补一个namei -l检查整条目录链路的权限确认 nginx 的 worker 用户能不能一路读进去。这套组合下来基本没有查不出来的路径问题。6. 容易被忽略的细节尾斜杠、权限与index的连锁反应6.1 尾斜杠的敏感性路径问题排查到后期最容易翻车的就是斜杠。第一处是 location 定义本身location /api和location /api/是两个不同的匹配范围。前者能匹配/api、/api/users甚至/apiX后者严格要求 URI 以/api/开头。如果前端和后端对斜杠的处理不一致就会出现有些接口通、有些接口 404的诡异现象。第二处是 proxy_pass 的尾斜杠。前面已经强调过一遍这里再补个细节对比proxy_pass http://127.0.0.1:8080;和proxy_pass http://127.0.0.1:8080/;在配置里只差一个字符行为却是透传和替换前缀两个完全不同的机制。改配置时手一抖少删一个斜杠排查一下午都是常有的事。第三处是 alias 的尾斜杠。alias 在部分组合下结尾斜杠有无会直接影响最终路径。一个典型场景location /static/ {}配alias /data/static;和alias /data/static/;请求/static/a.png时前者可能拼出/data/statica.png这种错误路径。因为/static/被替换成/data/static后剩余 URI 是a.png两者一拼就出问题加上结尾斜杠就没这事。我的建议是alias 的值末尾强制要求写斜杠尤其在 location 也以斜杠结尾的情况下别偷懒。6.2 文件权限与index指令的干扰路径拼对了文件也在但页面还是打不开下一个嫌疑就是权限和 index 的连锁问题。nginx 的 worker 进程通常以www-data或nginx用户运行它对目录要有读和执行权限对文件至少要有读权限。注意执行权限对目录来说就是是否能进入目录这个经常被忽略。排查时可以模拟sudo -u www-data ls -l /var/www/demo/dist/如果 nginx 用户都进不去那配置文件写得再对也白搭。另一个容易误判的是403 directory index is forbidden。这个报错的意思是你请求的 URI 以/结尾nginx 会去找 index 文件但你既没配index index.html;目录里也没有匹配的索引文件autoindex又是关闭状态那就只能回一个 403。很多人看到 403 第一反应是权限其实纯粹是 index 指令缺失。单独给这类目录设一个空 location 并关掉日志能让问题更早暴露。6.3 关于整套配合的个人操作习惯折腾了这么多轮之后我给自己定了几个不成文的规矩。一是 root 只写在 server 层location 里能不改就不改除非真的需要覆盖一旦 location 里出现了 root我必定在旁边注释里写上这里会拼出什么路径防止自己下次看配置时再绕进去。二是 alias 只在目录结构和 URL 前缀不对称时使用其他情况一律 root减少斜杠敏感带来的坑。三是 location 前缀能覆盖的场景尽量不用正则正则留着做捕获变量这类真正需要灵活处理的活。四是每次上线路径相关配置我都会执行一遍 curl 实测三条路径静态文件、接口转发、首页兜底确认三种机制都正常才收工。这套思路和习惯并不是什么高深的理论都是在一次次的打不开里磨出来的。nginx 的路径体系看着绕本质上就两件事本地文件怎么定位、请求往哪转发。root、alias、location、proxy_pass 这四个指令各司其职把它们的边界和配合关系理清了绝大多数路径拼接类的 404 和转发错乱问题都能在五分钟内定位并解决。