
Nginx 配置从入门到能配location 优先级、反向代理与 404/502 排查Nginx 是那种看两篇文章就能跑起来但配置出问题时会让你怀疑人生的软件。因为它和 Shell 脚本有个共同的坏毛病配置写错了它不报错。nginx -t显示语法正确、reload成功、服务正常——但你改的那条规则根本没生效请求走了另一条 location。这篇讲的就是这件事怎么知道你写的配置到底是哪一条在起作用。文章目录Nginx 配置从入门到能配location 优先级、反向代理与 404/502 排查零、先看这张速查表一、location 匹配顺序就是一切1.1 用一个例子看清楚1.2 三条实用规则二、server_name域名没匹配上的时候三、反向代理三个必须理解的细节3.1 proxy_pass 结尾那个斜杠3.2 必须传的几个头不然后端会瞎3.3 三个超时含义不同3.4 大响应体与缓冲四、静态资源root vs alias以及 SPA4.1 root 和 alias 的区别最容易搞混的一对4.2 单页应用SPAtry_files 那一行4.3 缓存策略分清能长缓存和不能缓存4.4 gzip五、HTTPS 与安全响应头六、限流Nginx 层面最实用的防护七、404 / 502 / 504 的排查路径7.1 404Nginx 说这个文件/路径不存在7.2 502Nginx 连不上后端7.3 504后端响应太慢八、运维层面的四件事8.1 改配置的正确流程8.2 reload 是热加载不会中断请求8.3 日志切割必须发 USR1 信号8.4 并发能力怎么算九、坑清单十、上线检查清单写在最后零、先看这张速查表大部分 Nginx 问题都能对号入座症状最可能的原因去哪一节404root和alias用混了或缺try_files第四节502 Bad Gateway后端没起来 / 端口写错 / 连不上第七节504 Gateway Timeout后端处理太慢超过了 proxy 读超时第七节改了配置但没生效没reload、改的不是生效的那份文件、location 优先级选错了第一节、第八节静态资源 403文件权限或目录缺x权限第四节后端日志里全是127.0.0.1反代没传X-Real-IP/X-Forwarded-For第三节按 IP 限流失效同上Nginx 看到的也全是同一个 IP第三节、第六节日志切割后磁盘不释放少发了USR1信号第八节其中改了没生效是最值得先解决的——因为它会让你在错误的方向上调试很久。一、location 匹配顺序就是一切这是 Nginx 最核心、也最容易配错的地方。先记住优先级顺序优先级写法含义1最高location /api/login精确匹配命中就停止不再往下找2location ^~ /static/前缀匹配命中后不查正则3location ~ \.php$区分大小写location ~* \.png$不区分正则匹配按在配置文件里出现的顺序第一个命中即用4最低location /api/普通前缀匹配取最长的那条完整的匹配流程按这个顺序走① 有 精确匹配吗 → 有就直接用结束 ② 找最长的前缀匹配 └─ 如果这条带 ^~ → 直接用结束 ③ 按出现顺序试所有正则 → 有一个命中就用结束 ④ 正则都没命中 → 用第 ② 步那个最长前缀1.1 用一个例子看清楚location /api/health { return 200 ok; } # ① 精确 location ^~ /api/static/ { root /data; } # ② 前缀不查正则 location ~ \.(jpg|png)$ { expires 7d; } # ③ 正则 location /api/ { proxy_pass http://backend; } # ④ 前缀兜底请求命中为什么/api/health①精确匹配优先级最高/api/static/a.png②^~命中后不再查正则虽然\.png$也能匹配/upload/a.png③没有前缀能匹配走正则/api/books④正则不匹配用最长前缀第 2 行和第 3 行的对比是本节的重点如果没有^~/api/static/a.png会被第 ③ 条正则抢走root /data就永远不生效——这就是典型的我配了但它没用。1.2 三条实用规则需要这个路径下所有东西都归我管时用^~——否则会被更靠前的正则抢走精确匹配只用于极少数固定路径健康检查、特定的回调地址它的作用是跳过所有后续判断正则要慎用——它们按出现顺序匹配加一条新的正则可能悄悄改变已有路径的行为。能用前缀就别用正则。二、server_name域名没匹配上的时候一个请求进来Nginx 先按listen端口分组再在组内按server_name找 server 块① 精确名字 server_name example.com; ② 以 * 开头的最长通配 server_name *.example.com; ③ 以 * 结尾的最长通配 server_name www.*; ④ 第一个匹配的正则 server_name ~^www\d\.example\.com$; ⑤ default_server 都没有 → 用 listen 上标了 default_server 的那个 ⑥ 最后兜底 没有 default_server → 用该端口上的第一个 server 块第 ⑤ ⑥ 条是常见坑的来源如果你的配置里没有显式指定default_server用 IP 直接访问会命中第一个 server 块通常是你配的第一个域名站点。所以经常出现我用 IP 访问居然看到了某个网站的现象——这不是被入侵是默认行为。建议给每个端口显式指定 default_server并让它返回 444Nginx 专有的直接断开连接server { listen 80 default_server; server_name _; return 444; # 不返回任何内容直接断开省得被扫描 }三、反向代理三个必须理解的细节反向代理是 Nginx 最常用的功能但下面三点不懂就会踩坑。3.1proxy_pass结尾那个斜杠这是最经典的坑差别只有一个字符行为完全不同# 情况 A结尾没有斜杠没有 URI 部分 location /api/ { proxy_pass http://backend; } # 请求 /api/books → 传给后端 /api/books URI 原样过去 # 情况 B结尾有斜杠有 URI 部分 / location /api/ { proxy_pass http://backend/; } # 请求 /api/books → 传给后端 /books location 匹配的部分被替换掉规则proxy_pass后面带了 URI哪怕只是一个/就会用这个 URI替换掉 location 匹配到的那部分没带 URI 就把完整路径原样传过去。实际怎么选后端接口路径该怎么写后端就是/api/booksproxy_pass http://backend;不带斜杠后端是/books前端调/api/booksproxy_pass http://backend/;带斜杠⚠️一个限制在正则 locationlocation ~里proxy_pass不能带 URI 部分。想剥掉前缀只能用前缀 location。3.2 必须传的几个头不然后端会瞎location /api/ { 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后端日志里所有请求的 IP 都是 Nginx 的127.0.0.1X-Forwarded-For经过多层代理时拿不到原始客户端 IP 链X-Forwarded-Proto后端不知道用户实际用的是 HTTPS可能重定向回 http死循环这两个 IP 头的后果比看起来严重限流、防刷按 IP 判断 →全部失效所有人都被当成同一个 IP安全审计日志 → 完全无用排查问题时 → 你无法知道是哪个用户在打你的接口。Spring Boot 侧要配合打开否则request.getRemoteAddr()拿到的还是 Nginx 的 IPserver:forward-headers-strategy:framework# 让框架读取 X-Forwarded-* 头3.3 三个超时含义不同proxy_connect_timeout 5s; # 和后端建连接的超时后端没起来时会很快暴露 proxy_send_timeout 60s; # 向后端发送请求的超时 proxy_read_timeout 60s; # 等待后端响应的超时 ← 504 就是它超了504 基本都是proxy_read_timeout到了。这时候不要去调大它——先想想为什么一个接口要跑 60 秒。调大超时只是把问题藏起来用户还是要等只是从 504 变成了长时间转圈。3.4 大响应体与缓冲Nginx 默认会把后端响应先缓冲到磁盘再发给客户端。如果你的接口返回一个大文件导出 Excel、下载这会变得很慢或者占满磁盘location /api/export/ { proxy_pass http://backend; proxy_buffering off; # 不缓冲边收边发 proxy_read_timeout 300s; # 导出慢单独放宽 }四、静态资源rootvsalias以及 SPA4.1root和alias的区别最容易搞混的一对同样是location /img/两者指向的文件路径完全不同# root把完整 URI拼在后面 location /img/ { root /var/www; } # 请求 /img/a.png → 文件 /var/www/img/a.png # alias用 alias 替换掉 location 匹配的部分 location /img/ { alias /var/www/img/; # 注意结尾这个斜杠 } # 请求 /img/a.png → 文件 /var/www/img/a.png两个容易记混的点root拼的是完整 URIalias是替换。所以只要目录名和 URI 不一致就得用aliasalias结尾的斜杠最好和 location 保持一致。location /img/配alias /var/www/img;不带斜杠会导致路径拼接出错。建议能用root就用root把服务器目录结构设计成和 URL 一致/var/www/img/对应/img/。alias只在目录结构没法对齐时才用。4.2 单页应用SPAtry_files那一行Vue / React 项目部署必然要用到这条location / { root /opt/app/frontend; try_files $uri $uri/ /index.html; }try_files是按顺序尝试返回第一个存在的参数含义$uri先找有没有这个文件/assets/index.js$uri/再找有没有这个目录/book//index.html都没有 →交给前端路由处理为什么必须有最后那个/index.htmlVue 是单页应用/book/123这个路径在服务器上并没有对应的文件。不加这行用户刷新页面就会 404——首页能进、一刷新就 404是 SPA 部署最典型的症状。4.3 缓存策略分清能长缓存和不能缓存# 带内容哈希的构建产物可以长缓存 location /assets/ { root /opt/app/frontend; expires 1y; add_header Cache-Control public, immutable; } # index.html绝对不能缓存 location /index.html { root /opt/app/frontend; add_header Cache-Control no-cache, no-store, must-revalidate; }逻辑很直接Vite / Webpack 给产物文件名加了内容哈希index-a1b2c3d4.js内容变了文件名就变了所以可以放心缓存一年而index.html里引用的就是这些文件名——它必须每次重新获取否则用户永远加载的是旧版本的页面。一句话带哈希的文件长缓存入口 HTML 不缓存。搞反了就会出用户明明发版了还看到旧页面的问题。4.4 gzipgzip on; gzip_vary on; # 加 Vary: Accept-Encoding让 CDN 正确缓存 gzip_min_length 1024; # 太小的文件压缩反而变大 gzip_comp_level 5; # 5 是性价比平衡点9 太吃 CPU gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svgxml;别开gzip on就完事默认gzip_types只包含text/html你的 JS 和 CSS 其实没被压缩。要显式列出来。五、HTTPS 与安全响应头这部分第 12 篇详细讲过这里只收拢关键配置server { listen 443 ssl; http2 on; server_name example.com; 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; add_header Strict-Transport-Security max-age31536000 always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; # 禁止访问隐藏文件.git、.env location ~ /\. { deny all; return 404; } }⚠️两个add_header的坑都属于配了但不生效子 location 里的add_header会覆盖父级的。如果某个 location 自己加了一条add_header父 server 块里那三条安全头在这个 location 下就全没了always参数别漏。不加它只在 2xx/3xx 响应上生效——而安全头恰恰在 404、500 这类响应上也需要。六、限流Nginx 层面最实用的防护这是运维味最重的部分也是应用层的限流替代不了的一层Nginx 在应用之前能挡住还没进业务逻辑的请求。# 定义限流区按客户端 IP10MB 内存每分钟 5 次 # $binary_remote_addr 只占 4 字节$remote_addr 最多 15 字节同样内存能存更多 IP limit_req_zone $binary_remote_addr zonelogin:10m rate5r/m; # 连接数限制 limit_conn_zone $binary_remote_addr zoneperip:10m; server { # 登录接口防暴力破解 location /api/auth/login { limit_req zonelogin burst3 nodelay; limit_req_status 429; # 默认返回 503改成 429 语义更准确 proxy_pass http://backend; } location /api/ { limit_conn perip 20; # 单 IP 最多 20 个并发连接 limit_conn_status 429; proxy_pass http://backend; } }burst和nodelay这两个参数决定了用户体验必须理解配置行为rate5r/m平均每分钟 5 次burst3允许突发多来 3 个请求排队等候超出就拒绝nodelay排队的这 3 个立即处理而不是按 rate 慢慢放不带nodelay会怎样第 2 个请求会被延迟处理等到速率允许的时刻——用户感觉卡住了。所以对交互接口基本都要加nodelay。⚠️limit_req_status别忘改默认限流返回503 Service Unavailable这会让前端以为是服务挂了而实际是请求太频繁。改成429 Too Many Requests语义才正确。另一个实用位限流单独开个日志级别不然被限流的请求会刷满 error.loglimit_req_log_level warn;七、404 / 502 / 504 的排查路径这三类占了 Nginx 问题的绝大多数。它们的排查方向完全不同所以第一步是分清是哪一个。7.1 404Nginx 说这个文件/路径不存在先确认文件到底应该在哪# 看实际生效的配置不是你看的那个文件是合并后的结果nginx-T|grep-A15server_name example.com# 手动验证文件在不在把 URI 拼到 root 后面ls-l/opt/app/frontend/book/123# 直接从本机请求看 Nginx 返回什么curl-Ihttp://127.0.0.1/book/123三个高频原因原因判断方法SPA 缺try_files首页正常、刷新子路由 404 → 就是它root/alias用混用nginx -T看最终生效的是哪个再按第四节对比路径权限不够导致返回 404看 error.log 里是否有Permission denied有时 Nginx 对权限问题返回 404 而非 4037.2 502Nginx 连不上后端502 的本质是网关联系不上上游所以排查重点是能不能连通# ① 后端进程在不在systemctl status jush-book ss-lntp|grep8080# ② 绕过 Nginx直接请求后端 —— 这一步能立刻分清是谁的问题curl-vhttp://127.0.0.1:8080/api/books# ③ 看 Nginx 的 error.log里面会写清楚原因tail-50/var/log/nginx/error.log常见 error.log 内容与含义报错原因connect() failed (111: Connection refused)后端没起来或者端口不对upstream timed out后端在跑但没响应 → 转去看 504 的思路no live upstreams所有后端节点都被标记为不可用Permission deniedSELinux 拦截CentOS/RHEL 上httpd_can_network_connect没开Connection refused占了 502 的一大半——所以排查 502 的第一步永远是后端到底在不在跑而不是先去改 Nginx 配置。7.3 504后端响应太慢# 后端慢在哪先看应用日志里的耗时tail-f/opt/app/logs/app.log# 看是不是数据库拖住了呼应第 10 篇mysql-eSHOW FULL PROCESSLIST504 的正确处理顺序先确认是不是所有接口都慢——只有个别接口慢就是那接口的问题慢 SQL、N1、外部调用超时再看是不是并发高时才慢——那可能是连接池打满第 9 篇讲过 HikariCP 才是真实并发上限最后才考虑调proxy_read_timeout并且要同时明确这个接口的正常耗时上限是多少。⚠️ 反过来说一句调大proxy_read_timeout是最常见的错误处理。它让 504 变成一直转圈用户体验更差而且你把证据超时错误也擦掉了。八、运维层面的四件事8.1 改配置的正确流程nginx-t# ① 先测语法别直接 reloadnginx-T# ② 打印合并后的最终配置确认你改的确实生效了nginx-sreload# ③ 热加载nginx -T是本节最值得记住的命令大写的 TTest dump它打印的是 Nginx实际加载的完整配置所有include展开后的结果。当你改了文件但没生效时用它一眼就能看出是不是改错了文件、是不是被后面的 include 覆盖了、最终生效的到底是哪一条 location。# 直接搜你关心的那一段nginx-T|grep-n-A10location /api/8.2reload是热加载不会中断请求nginx-sreloadreload的机制是启动新的 worker 进程接管新连接老的 worker 处理完手上的请求后才退出。所以用户的请求不会中断这是它比restart好的地方但老 worker 可能还会存活几秒到几十秒取决于有没有长连接、大文件下载。这意味着如果你同时改了配置和比如证书文件切换期间新旧配置是并存的。要严格一致就得restart代价是短暂中断。⚠️reload也有失败的可能如果配置有语法错误reload 会失败并保持旧配置继续运行服务不会挂——这是好事但你必须看命令输出才知道它失败了否则会出现我 reload 了但它还是老行为。8.3 日志切割必须发USR1信号这是个和 Shell 那篇里讲过的删了文件磁盘不释放同类的问题直接mv或rm日志文件Nginx 手里的句柄还指向老文件磁盘空间不会释放。正确做法是切割后通知 Nginx 重新打开日志文件——它收到USR1信号就会这么做# /etc/logrotate.d/nginx/var/log/nginx/*.log{daily rotate14missingok notifempty compress delaycompress sharedscripts postrotate[-f/run/nginx.pid]kill-USR1$(cat/run/nginx.pid)endscript}为什么这里不用copytruncate第 5 篇讲 Java 日志时用过copytruncate因为进程持有句柄没法重开。Nginx 支持USR1信号重开日志所以用postrotate 信号是更好的做法——copytruncate有个短窗口会丢日志。判断要不要copytruncate的标准很简单程序支不支持重开日志文件的信号。支持就用postrotate不支持很多 Java 程序才退而用copytruncate。8.4 并发能力怎么算worker_processes auto; # 一般设为 CPU 核数 events { worker_connections 1024; # 单个 worker 的最大连接数 }理论最大连接数 worker_processes×worker_connections。⚠️但做反向代理时要打个对折Nginx 既要接客户端连接又要连后端一个请求占两个连接。所以 2 核 × 1024 的配置实际能扛的并发请求大约在 1000 量级不是 2048。另外别忘了文件描述符限制# 系统级ulimit-n# 查看# Nginx 配置里可以显式提高worker_rlimit_nofile65535;默认的 1024 会在并发上来时成为瓶颈报错是too many open files——这个错误很直观但很多人不知道要在 Nginx 和系统两个层面都改。九、坑清单#坑后果与解法1前缀 location 忘了^~被后面的正则抢走“我配了但没用”。需要独占用^~2proxy_pass斜杠写反404。带/会剥掉 location 前缀不带则原样传3没传X-Real-IP后端日志和限流里所有请求都是127.0.0.14没传X-Forwarded-Proto后端以为用户在用 http重定向成死循环5SPA 缺try_files ... /index.html首页正常、刷新子路由 4046index.html被长缓存发版后用户看到的还是旧页面。入口 HTML 必须no-cache7gzip on但没配gzip_typesJS/CSS 其实没压缩默认只有 text/html8子 location 里写了add_header覆盖掉父级的所有安全头父级那几条在这个 location 下失效9add_header没加always只在 2xx/3xx 生效404/500 响应上丢了安全头10limit_req用默认状态码返回 503 让前端误判服务挂了。应改limit_req_status 42911limit_req没加nodelay突发请求被延迟处理用户感觉卡顿12日志用mv切割磁盘空间不释放。要用USR1信号或copytruncate13用root配了不匹配的目录结构路径拼错 → 404。目录和 URL 不一致就用alias14ulimit -n保持默认 1024并发高时报too many open files15改完直接用reload不看输出reload 失败时会静默沿用旧配置你以为改了第 1、2、5、8 条属于配置看着对但行为不对是最典型的静默生效错误配置。十、上线检查清单nginx -t通过且nginx -T确认生效的是你以为的那份配置每个端口都有显式default_server建议返回 444proxy_pass的斜杠方向已按后端实际路径确认过反代已传Host/X-Real-IP/X-Forwarded-For/X-Forwarded-ProtoSpring Boot 侧已开server.forward-headers-strategy: frameworkSPA 有try_files $uri $uri/ /index.htmlindex.html是no-cache带哈希的静态资源是长缓存gzip_types里包含application/javascript和text/css敏感路径已 denylocation ~ /\.登录接口有限流且limit_req_status是 429、带了nodelay日志切割用USR1信号rotate天数已定worker_rlimit_nofile和系统ulimit -n都调过HTTPS 证书自动续期验证过certbot renew --dry-run见第 12 篇配置已纳入版本管理改坏了能回滚写在最后Nginx 的配置语法其实很小难的不是怎么写而是写的这条到底有没有在起作用。所以这篇里所有内容其实都在回答同一个问题怎么知道你的配置真的生效了。三个手段nginx -T——看合并后的最终配置而不是你编辑的那个文件nginx -t 看输出——reload 可能失败且失败是静默的curl -verror.log——从客户端和后端两个方向看请求到底走没走通。最后给一句我踩过坑之后的总结Nginx 配置出问题时先别改配置先确认当前生效的配置是哪几行。顺序搞反就会在错误的方向上改很久。互动时间你有没有过改了 Nginx 配置但怎么都不生效的经历最后是什么原因——改错了文件、被后面的 include 覆盖、还是 location 优先级选错了评论区说说这类问题互相提醒特别有用。觉得有用点个收藏nginx -T那条真的能省很多时间。系列回顾《运维到底是干什么的》 · 《运维工程师该学哪些技能》 · 《Docker 到底是什么》 · 《Docker Compose 一键部署》 · 《上线之后必须做的四件事》 · 《线上故障排查实录》 · 《Linux 常用命令速查手册》 · 《用 GitHub Actions 自动部署》 · 《一个人把二手图书交易网站做到上线》 · 《MySQL 索引与 EXPLAIN 从入门到会用》 · 《从零搭一套监控告警体系》 · 《HTTPS 与安全加固》 · 《Redis 从入门到能运维》 · 《Shell 脚本实战》