1. 为什么前端要自己折腾 Nginx先搞懂这个服务到底解决了什么问题先说个场景。某个跨端后台管理系统上线了前端同事本地npm run dev跑得飞起接口、代理、热更新一切正常结果一上测试环境就各种 404、接口 502、刷新路由白屏。后端说接口没问题运维说环境没毛病前端打开控制台一看静态资源加载路径全乱套。最后定位到问题出在 Nginx 配置上——root指错了目录、location匹配顺序不对、proxy_pass末尾少了个斜杠一个小问题折腾了一整天。这类问题在前后端分离的开发模式下太常见了。Vue、React、Angular 这套工具链出来之后前端不再只是写页面你写的代码最终要跑在一个 Web 服务上而这个服务十有八九就是 Nginx。作为高级前端架构方向的人你不一定天天配它但你必须懂它因为很多线上问题的根源就藏在那一百多行配置里。Nginx 本身能干的事非常多托管静态资源、反向代理、负载均衡、HTTPS 终止、Gzip 压缩、访问控制、日志记录。前端日常打交道最多的就是前三样。这篇文章我按自己踩坑总结的顺序来写先讲怎么装、怎么理解配置结构再讲静态站点怎么配、反向代理怎么配然后讲 HTTPS、Gzip、日志这些高频场景最后把常见错误和排查思路挨个过一遍。内容不追求大而全重点放在你真正会用到的部分配置示例全部拿来就能改。适合看这篇文章的人有两类一类是刚接触 Nginx 的前端需要把基础概念和配置逻辑理顺另一类是写了几年业务代码、想往架构方向走的朋友需要一套能直接落地、踩过坑之后验证过的配置思路。如果你是运维老手或者已经在生产环境摸爬滚打过几年可以直接跳过基础部分重点看后面的排错思路。我最初开始搞 Nginx 配置的时候心里想的就是一件事别让运维同事天天因为前端的问题来找我。后来发现这个目标太低了真正把 Nginx 搞明白之后前端能做的事比想象中多很多——比如灰度发布的前端版本控制、SPA 路由的回退策略、接口的灰度切换这些都离不开 Nginx 这一层。这篇文章算是基础篇先把最核心的 Web 服务配置吃透后面的进阶内容才有得聊。2. 环境和目录装好 Nginx 只是第一步看懂它的骨架才不迷路2.1 安装方式与版本选择用包管理器还是编译安装Nginx 的安装方式主要看你在什么系统上干活。我平时接触的服务器大部分是 Linux 发行版Deal 系列、Ubuntu 系列都有前端本机则是 macOS 和 Windows 混着来。Linux 上最简单的方式是直接用系统包管理器。Ubuntu、Debian 用aptCentOS、AlmaLinux 系用yum或dnf。一条命令装完还能跟着系统一起做安全补丁省心。但有个坑系统自带源里的 Nginx 版本往往偏旧比如 Ubuntu 20.04 的源里默认版本是 1.18而官方最新稳定版已经到 1.24 以上。如果你不需要特别新的特性系统源完全够用如果你想体验http2、http3相关新能力或者想用某些只有新版本才有的指令建议加官方源或者直接编译安装。编译安装听起来高端其实适合的场景没那么玄乎。我之前编译过一次就为了加一个动态 TLS 模块和 Brotli 压缩模块打开官方源码包的configure脚本把需要的模块用--add-module指进去然后make make install。编译过程本身不难难的是依赖环境的坑——gcc、pcre-devel、zlib-devel、openssl-devel缺一不可。个人建议如果不是有特殊模块需求先用包管理器装着跑通流程等真正需要自定义模块的时候再上编译方案。macOS 上用brew install nginx最省事装完配置文件在/usr/local/etc/nginx/Apple Silicon 芯片则是/opt/homebrew/etc/nginx/默认站点目录在/usr/local/var/www/。Windows 用户就不用折腾了直接下载官方 Windows 二进制包解压就能跑但注意 Windows 版的 Nginx 性能表现不如 Linux 版本而且有些模块在 Windows 上不可用。做本地开发体验一下完全可以生产环境老老实实上 Linux。2.2 主配置文件与目录结构每一层都有它的用途Nginx 的配置体系理解起来不难掌握了核心逻辑就能举一反三。主配置文件一般叫nginx.conf它像一棵树的根通过include指令把其他目录里的子配置文件挂载进来。我常用的一套结构是这样的# /etc/nginx/nginx.conf user nginx; worker_processes auto; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; include /etc/nginx/conf.d/*.conf; }注意最后一行include /etc/nginx/conf.d/*.conf这才是核心——它说明你可以把每个站点的配置单独放一个文件放在conf.d目录或者sites-enabled目录下彼此独立、互不干扰。我习惯一个站点一个文件比如my-site.conf、admin-api.conf。改一个站点配置不用动全局文件出问题也好定位删掉这个文件就相当于下线了该站点极其清爽。如果你安装的是 Debian/Ubuntu 系的 Nginx还会看到sites-available和sites-enabled两个目录前者放所有可用的站点配置后者通过软链的方式启用指定的配置。这个设计在管理多个站点时挺实用把某站点临时下线就unlink一下不用直接删文件。但从简单好用的角度我自己更偏爱直接用conf.d的平铺方式一个目录下所有.conf文件都是启用的配上include /etc/nginx/conf.d/*.conf;即可。2.3 生命周期管理改完配置别急着重启先验证再平滑加载运行中的 Nginx 操作有几个常用命令任何一个合格的配置流程都离不开它们。我第一次接触 Nginx 时干过一件蠢事改完配置文件直接nginx -s restart结果配置语法有误生产环境的服务直接断了。后来学乖了改配置后先验证再重载顺序严谨到形成肌肉记忆。# 1. 测试配置语法 nginx -t # 2. 平滑重载配置不中断正在处理的请求 nginx -s reload # 3. 快速停止 nginx -s stop # 4. 完整停止 nginx -s quitnginx -t是最重要的一道防线它会输出配置文件的语法检查结果报错会明确告诉你哪个文件第几行出了问题。nginx -s reload是平滑重载它不会中断当前正在处理的请求只会让 worker 进程在处理完手上的活之后切换到新配置。生产环境更新配置几乎只用 reload绝不用 restart。还有一个小细节值得注意nginx -s里的-s是发送信号的意思需要主进程处于运行状态才能响应。如果配置里的pid路径被改过或者其他原因导致未能找到主进程就需要通过kill命令直接给主进程 PID 发信号。不过日常场景够不到那么深掌握前面几个命令即可。提示改完任何配置哪怕是加一行注释也建议先nginx -t再 reload。这个习惯能帮你避免 99% 的改了配置没生效或者改挂了的问题。3. 静态站点的第一个 Hello World从零搭一个能跑的前端页面3.1 最小可运行的 server 块先让页面能访问再谈优化静态站点是 Nginx 最常见的用途之一。一个前端项目构建完之后产生一堆 HTML、JavaScript、CSS、图片文件你要做的就是把 Nginx 指向这个目录让它可以被外部访问。完成这个目标一个server块就够了。先看最简单的例子。假设你有一个构建产物目录/data/www/my-app里面是打包出来的index.html和静态资源。那配置文件可以这么写server { listen 80; server_name my-app.example.com; root /data/www/my-app; index index.html; }解释一下这三行配置的含义。listen 80对应监听服务器的 80 端口这是 HTTP 协议默认端口server_name是当前站点的域名访问my-app.example.com时 Nginx 会把请求交给这个server块处理root定义了站点的根目录请求路径会直接映射到这个目录下的文件index则是指定访问目录时默认加载的文件用户访问my-app.example.com/时Nginx 会去根目录找index.html。用curl验证一下curl -I http://my-app.example.com/如果返回HTTP/1.1 200 OK和Content-Type: text/html说明最小站点已经跑起来了。到这里就完成了一个最简单不过的静态站点。实际项目中不会这么简单但任何复杂的配置都是从这个地基上长出来的。3.2 location 匹配规则与优先级为什么你的图片加载不出location是 Nginx 配置里最难啃的一块骨头因为它的匹配规则有优先级。很多前端小伙伴兴冲冲配了半天结果发现某些路径死活不走自己预期的行为看完匹配顺序才恍然大悟。Nginx 的location匹配遵循一个明确的优先级链条我从实践角度整理成一句话精确匹配 最长前缀匹配^~ 正则匹配~或~* 普通前缀匹配。用一个例子来说明。假设配置文件里有这么几个locationlocation /logo.png { # 精确匹配优先级最高 } location ^~ /static/ { # 前缀匹配并且不再检查后面的正则 } location ~ \.(gif|jpg|png)$ { # 正则匹配区分大小写 } location / { # 兜底匹配所有 }当请求路径是/logo.png时Nginx 会先看有没有精确匹配 /logo.png有就直接用后面的规则全部不看了。当请求路径是/static/img/a.jpg时命中^~ /static/这个前缀匹配因为带了^~Nginx 会停止继续搜索正则。如果请求是/photo/me.jpg会走到正则~ \.(gif|jpg|png)$这里。如果什么都不匹配就落到location /兜底。优先级顺序不是死记硬背来的我在实际项目中踩过的坑更能说明问题。某次给一个后台管理系统配资源路径/api/前缀的接口要走反向代理/assets/前缀的静态文件要直接读磁盘。我一开始把静态文件的 location 写成了location /assets没过多久就发现部分请求进了代理分支原因就是请求路径正好以/assets开头但又带了一个斜杠尾巴匹配规则的边界没理清楚。后来统一改用不带尾部斜杠的写法和^~修饰行为才彻底稳定下来。下面这组规则是我现在项目里常用的一个静态站点配置模板你可以直接参考server { listen 80; server_name static.example.com; root /data/www/app; # 静态资源不带正则优先匹配 location ^~ /static/ { expires 7d; add_header Cache-Control public; } # 图片文件走正则匹配 location ~* \.(png|jpg|jpeg|gif|ico|svg|webp)$ { expires 30d; add_header Cache-Control public; } # 默认回退 location / { try_files $uri $uri/ /index.html; } }这里用到的try_files是 SPA 路由历史模式的灵魂配置后面单独讲。3.3 root 和 alias同样是指向目录行为完全不一样root和alias这两个指令是前端最容易混淆的配置点搞错之后文件路径会整体错位。两者表面上都是指定一个文件系统路径但拼接规则完全不同。root的处理方式是请求路径直接拼在 root 后面。比如location /static/ { root /data/www; }请求/static/js/app.js时Nginx 会去找/data/www/static/js/app.js。也就是说root是根location 那一段路径也会被保留在最终路径里。alias的处理方式是用 alias 指定的路径替换掉 location 匹配的那一段。比如location /static/ { alias /data/files/; }请求/static/js/app.js时Nginx 找的是/data/files/js/app.jslocation 前缀/static/完全被替换掉了。两者的差异用一句话概括root拼接的是完整请求 URIalias替换的是匹配部分。从实际使用的角度root用在server级或者location /下做全局根目录alias适合把某个 URL 前缀映射到服务器上一个完全不相关的目录。有一个容易踩的坑是alias路径末尾的斜杠问题如果 location 是/static/带斜杠alias 最好也带上斜杠/data/files/带斜杠否则拼接时容易多出一个或缺少一个斜杠。比如location /static/配alias /data/files不带斜杠请求/static/a.js时拼接的结果是/data/filesa.js路径完全错乱报错信息还不太直观。建议在配alias之前先做一个简单的路径推演拿一个实际请求 URI 套进规则里看拼接结果是否符合预期。我习惯在本地把目录结构搭好配完用curl实测避免直接上生产再手忙脚乱。3.4 单页应用路由回退刷新页面 404 的根源与解决方案SPA单页应用模式下Vue Router 或 React Router 用history模式时路由路径是真实的 URL比如/user/123、/settings/profile。前端路由切换是在浏览器端完成的不需要请求服务器但用户在这个路径下按 F5 刷新浏览器就会向服务器发起/user/123的请求。如果 Nginx 找不到这个路径对应的文件自然返回 404。解决思路是把所有不存在的路径都回退到index.html由前端路由自己根据 URL 渲染对应页面。这就是try_files指令的典型使用场景location / { root /data/www/app; try_files $uri $uri/ /index.html; }try_files的逻辑从配置文本里就能看出来先尝试$uri也就是请求路径本身再尝试$uri/即把路径当目录处理尝试找目录下的index文件如果都没有就返回/index.html。最终效果是对于任意一个前端路由路径Nginx 都会把index.html交还给浏览器。这个方案有个需要注意的地方它会把所有不存在的路径包括 404 的真实资源都打进index.html导致前端拿到的 HTTP 状态码始终是 200即使页面本身是一个页面不存在的提示。从 SEO 角度看这种做法并不理想很多团队会选择在 Nginx 层对robots.txt或特定的静态目录单独配置不加入回退逻辑保证搜索引擎爬虫能正确获取信息。另一个小坑是try_files $uri $uri/ /index.html中的最后一个参数必须是内部跳转的 URI不能是外部 URL。如果你试图把它写成https://xxx.com/index.html这种形式Nginx 会直接报错。内部跳转会触发 Nginx 的 internal redirect相当于重新走一遍 location 匹配如果你对/index.html也做了特殊的 location 定义注意别形成死循环。提示如果你的项目部署在子路径下比如https://example.com/admin/那么try_files的回退地址要改成/admin/index.html同时前端构建时也要配置好基础路径。子路径部署的坑深且多后面文章我再单独展开。4. 反向代理与负载均衡前端接入后端接口的关键一跳4.1 为什么要用反向代理而不是让前端直连后端服务前后端分离的开发模式下前端静态页面和后台 API 往往跑在不同的服务上。前端在http://www.example.com后端接口在http://api.internal.example.com:8080。如果让浏览器直接向后端服务发请求首先会遇到跨域问题其次后端的真实 IP 和端口暴露在公网也不是什么好事。Nginx 反向代理解决的就是这件事。它监听前端域名的请求然后把符合特定规则的请求转发给后端服务再把后端返回的数据原样回传给浏览器。对于浏览器来说它始终只跟 Nginx 一个服务打交道既解决了跨域又做了一层转发隔离。一个最基础的代理配置长这样server { listen 80; server_name www.example.com; # 静态资源直接本地读 location /assets/ { root /data/www/web; } # 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; } }proxy_pass指定后端服务的地址proxy_set_header用来把客户端的原始信息透传给后端。这里有一个很关键的细节后端服务拿到的 HTTP 请求头里Host字段如果不改写会是 Nginx 自身的域名或 IP而不是浏览器访问时用的那个域名。很多后端框架在做域名校验或者生成绝对链接时依赖Host字段所以这个头必须带上。X-Real-IP和X-Forwarded-For则是为了让后端知道真正的客户端 IP毕竟从 Nginx 转发过去的请求源 IP 已经变成了 Nginx 所在机器。这套方案在开发环境下也常用。比如本地前端跑在localhost:3000后端服务在localhost:8080前端通过 Vite 或 Webpack Dev Server 的代理功能做转发原理跟 Nginx 的proxy_pass是完全一致的。理解 Nginx 的反向代理再去看前端脚手架里的 proxy 配置思路就是相通的。4.2 proxy_pass 的斜杠陷阱一个字符之差API 路径面目全非proxy_pass后端地址的尾部是否带斜杠会直接影响转发后 URI 的拼接结果。这个细节在 Nginx 的所有配置项里应该算最容易忽略、后果最诡异的一类。先看情况一location和proxy_pass都没有额外路径。location /api/ { proxy_pass http://127.0.0.1:8080; }请求/api/user/list转发后 URI 是/api/user/list后面的路径完全是原样转发。情况二proxy_pass带了路径。请求/api/user/list。location /api/ { proxy_pass http://127.0.0.1:8080/v2/; }转发后 URI 是/v2/user/list。这里发生了替换location匹配到的/api/部分被替换成了/v2/两个斜杠之间严格做了一次前缀替换。如果你把proxy_pass写成http://127.0.0.1:8080/v2不带尾部斜杠转发后 URI 会变成/v2user/list后端收到这个请求直接 404。情况三location是精确匹配时情况又不同。如果写location /api不带斜杠proxy_pass http://127.0.0.1:8080/请求/api会被转发成/。带不带路径取决于你希望后端收到什么样的 URI。实操建议是配置proxy_pass时统一带上尾部斜杠并且在改完配置后用nginx -t加一次实际请求验证。我在给一个小程序的后端接口做网关转发时因为proxy_pass少了末尾斜杠排查了大半天最后用curl -v看到转发报文才发现问题。这个看似不起眼的字符影响范围是整个接口层。4.3 负载均衡 upstream轮询、权重、ip_hash 怎么选如果你有多个后端实例Nginx 的upstream模块可以帮你做负载均衡。虽然这个方向偏运维但前端架构师在规划服务拓扑时也会经常碰到至少要知道基本配置和选择逻辑。upstream backend_pool { server 192.168.1.10:8080 weight3; server 192.168.1.11:8080 weight1; server 192.168.1.12:8080 backup; } server { listen 80; server_name api.example.com; location /api/ { proxy_pass http://backend_pool; proxy_set_header Host $host; } }这段配置里定义了一个名为backend_pool的服务器组里面三个后端实例前两个承担轮询流量权重比是 3:1第三个标为backup平时不参与服务只有前两台都异常时才启用。proxy_pass的地址直接写http://backend_poolNginx 就会把请求按策略分发到组内实例。负载策略的选择逻辑可以从实际场景来理解。默认的轮询方式适合后端服务完全无状态、处理能力相近的场景。加权轮询适合机器配置参差不齐的情况性能强的机器权重调大多承担一些流量。ip_hash则是把同一客户端的请求固定分发到同一台后端服务器适合那些登录态或 Session 存在单个实例内存里的老项目。upstream backend_pool { ip_hash; server 192.168.1.10:8080; server 192.168.1.11:8080; }我碰过一种典型情况某老商城项目的后端没有改造 Redis 共享会话登录用户的 Session 存在单台实例内存里一刷新页面用户就掉线。后来在 Nginx 上加了ip_hash才勉强解决 Session 在不同实例间跳来跳去的问题。当然这个方案不是治本之策治本还是后端要改造成无状态服务但前端架构同学能理解这个层面的权衡排查问题时会快很多。除了分配策略upstream里还可以配连接超时、失败重试等参数这些进阶内容后面有机会单独写。基础的负载均衡先把轮询、权重、backup和ip_hash搞明白应付绝大多数应用场景足够了。5. HTTPS、Gzip 与日志上线前必须做好的三件事5.1 HTTPS 证书配置让浏览器的小锁变绿redirect 也不能出错HTTP 明文传输的风险不言而喻生产环境现在基本都是全站 HTTPS。前端如果想部署自己的站点至少要知道证书怎么挂、HTTP 怎么跳 HTTPS。证书来源最常见的两个云厂商提供的免费证书通常一年一签以及 Lets Encrypt 的自动续期证书。个人的基础站点完全可以用 Lets Encrypt 方式配合certbot工具一条命令就能完成证书申请和 Nginx 配置。sudo certbot --nginx -d www.example.com -d example.com这个命令会帮你自动申请证书、修改 Nginx 配置、设置定时续期任务。整个过程体验非常顺畅我建议个人项目的第一次 HTTPS 配置直接用certbot省去手动处理证书路径和续期的麻烦。如果是手动配置证书Nginx 的写法是这样的server { listen 443 ssl http2; server_name www.example.com; ssl_certificate /etc/nginx/ssl/www.example.com.pem; ssl_certificate_key /etc/nginx/ssl/www.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; root /data/www/web; index index.html; } server { listen 80; server_name www.example.com; return 301 https://$host$request_uri; }第一个server块监听 443 端口并启用 SSLhttp2选项开启 HTTP/2 协议。第二个server块监听 80 端口把所有 HTTP 请求用 301 重定向到 HTTPS 版本。这里需要注意return 301的写法$host保留原始域名$request_uri保留原始路径这样http://www.example.com/user/123重定向后能正确到达https://www.example.com/user/123路径信息一点不丢。还有一个容易忽略的坑如果你配置了 HSTSHTTP Strict Transport Security头浏览器会强制使用 HTTPS 访问你的域名即使你不再提供 80 端口服务用户也无法通过 HTTP 访问。HSTS 的配置头是这样add_header Strict-Transport-Security max-age31536000; includeSubDomains always;这个头建议只在完全确定 HTTPS 稳定之后再加因为一旦加了有效期内的浏览器访问策略就锁死了排错过程中的回退操作会受到阻碍。5.2 Gzip 压缩配置让前端资源体积缩小 60% 的性价比之王Web 性能优化有一个性价比极高的手段Gzip 压缩。文本类资源HTML、JavaScript、CSS、JSON、SVG经过 Gzip 压缩后体积能减少 60% 甚至更多而 Nginx 开启 Gzip 只需要几行配置。gzip on; gzip_comp_level 5; gzip_min_length 1k; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss text/javascript image/svgxml; gzip_vary on;参数里的gzip_comp_level控制压缩级别范围 1 到 9级别越高压缩率越高但 CPU 消耗也越大。实测下来5 左右是实际项目中性价比比较高的选择压缩率已经比较理想也不会给服务器造成明显压力。gzip_min_length 1k表示只有超过 1KB 的文件才压缩那些本来就只有几百字节的小文件压缩收益极低反而浪费时间。gzip_types列出要压缩的 MIME 类型这里要特别提醒JavaScript 文件需要写application/javascript老版本配置里写text/javascript在新规范下有兼容问题两个都写上最稳妥。图片要不要压缩这个在配置时容易产生误区。JPEG、PNG、WebP 本身就是压缩过的二进制格式Gzip 对它们的压缩率几乎为零完全没必要纳入压缩范围。但 SVG 虽然是图片格式本质是 XML 文本Gzip 效果非常好所以我在gzip_types里写上了image/svgxml。验证 Gzip 是否生效可以用curlcurl -H Accept-Encoding: gzip -I http://www.example.com/js/app.js如果响应头里出现了Content-Encoding: gzip说明压缩已生效。如果怎么配都不出来优先检查是不是遗漏了gzip on;这一行或者请求头里的Accept-Encoding不包含 gzip也可能动了gzip_disable相关配置。曾经有同事配了一下午没生效最后发现是gzip on被误删了感觉这种低级错误才是常态。5.3 访问日志与错误日志出问题的第一现场日志是排错时最重要的工具没有之一。Nginx 的日志体系分成访问日志access log和错误日志error log各自有各自的价值。访问日志记录每一个 HTTP 请求的关键信息默认情况下会包含客户端 IP、访问时间、请求方法、URI、HTTP 状态码、响应字节数、User-Agent 等字段。配置格式和日志路径可以自定义log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent ; access_log /var/log/nginx/access.log main;这里的log_format定义了日志行的格式main是格式的名称你可以定义多个不同的格式然后在access_log指令中按名称引用。默认的combined格式已经够用但自定义格式可以按需增加字段比如记录$request_time请求耗时和$upstream_response_time上游服务响应耗时排查接口性能问题时这个字段非常有用。错误日志记录 Nginx 运行过程中的错误信息包括配置错误、连接超时、上游服务不可达等。错误日志的配置error_log /var/log/nginx/error.log warn;日志级别从低到高是debug、info、notice、warn、error、crit、alert、emerg级别越高记录的内容越少。生产环境一般用warn或error级别排错时可以临时把error_log级别调到info甚至debug用完再调回来避免日志量过大。排查一个请求的完整链路时最简单的方式是把 access log 和 error log 结合起来看access log 里能看到请求的状态码和上下游响应时间error log 里能看到具体的连接问题或者权限问题。曾经有次排查接口偶发 502access log 里status显示 502 但看不出原因切到error.log一眼就看到upstream prematurely closed connection while reading response header from upstream直接定位到是后端服务的连接行为问题跟 Nginx 配置无关。这就是两种日志配合起来排查的效率优势。提示一旦上线了 HTTPS建议把访问日志里的协议版本也记录下来$server_protocol方便确认是否有大量 HTTP 流量没有正确跳转到 HTTPS这个指标对排错很有用。6. 高频问题排查实录404、502、403 这些状态码背后的真相6.1 404先分清是文件不存在还是路由没回退遇到 404很多人第一反应是后端接口挂了实际上 Nginx 层的 404 原因可以分为几类每类排查路径都不一样。第一类是文件路径确实不对。root或alias指定错了目录请求的 URI 在目标目录下找不到文件。这类问题最好的排查方式是curl -v请求具体路径看 Nginx 试图访问的文件系统路径。你可以用nginx -T把解析后的完整配置输出出来检查root和location的关系。也可以在配置里临时加上add_header X-debug-uri $uri;在响应头里输出 Nginx 解析后的 URI虽然add_header更适合调试其他场景但配$uri也方便。第二类是 SPA 路由没有回退。访问前端路由路径时刷新页面 404那就是try_files没有配置或者回退地址不对。按前面 3.4 节的方案配置即可。第三类是静态资源的请求路径带了 CDN 前缀或者版本参数Nginx 找不到实际文件。这种情况要看资源 URL 是否正确、构建产物里的 base 路径和部署目录是否匹配。曾经遇到过一个 Vue 项目配置了publicPath为/app/但 Nginx 的location只匹配/assets/结果所有 JS/CSS 全部 404改完 base 路径就好了。排查 404 我自己的顺序先curl -v看实际请求路径和响应头再到服务器上ls验证目标文件是否存在最后检查 location 规则有没有被其他高优先级规则截胡。前三步搞不定才需要开错误日志看细节。6.2 502 与 504后端服务的连接问题别急着甩锅 Nginx502 Bad Gateway 表示 Nginx 作为代理无法从上游服务器获得有效响应504 Gateway Timeout 表示上游服务在指定时间内没有返回结果。两种情况本质上都指向一个方向——后端服务出了问题。502 最常见的成因是后端服务没有启动或者崩溃。排查顺序是先确认后端端口有没有在监听netstat -tlnp | grep 8080如果端口没监听说明后端服务确实没起来。如果端口正常再用curl直接从服务器本机访问后端服务curl http://127.0.0.1:8080/api/health这一步能帮你很快定位问题边界本机访问后端正常那就是 Nginx 和后端之间的连接配置问题本机访问也不正常那就是后端服务本身的故障。还有一种常见情况后端服务的进程在但数据库连接池满了或者线程阻塞导致 Nginx 等待超时。这种情况会表现为访问日志里偶发 502 或 504错误日志里能看到upstream timed out的字样。504 超时的时间可以通过 Nginx 配置调整。proxy_connect_timeout控制建立连接的超时时间proxy_read_timeout控制从后端读取响应的超时时间。默认值通常是 60 秒如果你的后端服务有大批量导出或者长时间计算的场景可能就需要调大location /api/ { proxy_pass http://backend_pool; proxy_connect_timeout 10s; proxy_read_timeout 120s; }proxy_connect_timeout不建议设置太大因为正常情况下建立 TCP 连接是毫秒级别的设大了反而会拖长故障感知时间。proxy_read_timeout则要根据后端接口的真实耗时来定先观察后端日志里处理请求的时间分布再给一个合理的余量通常 30 到 60 秒足够。6.3 403目录权限和索引文件的隐形墙403 Forbidden 在 Nginx 场景下通常有两个原因文件系统权限不够或者目录下没有索引文件且禁止了目录浏览。权限问题常见的情形是网页目录的所有者是 root但 Nginx worker 进程以 nginx 用户身份运行导致 Nginx 没有读权限。排查方式ls -la /data/www/my-app ps aux | grep nginx看目录权限和 Nginx 进程运行的用户。解决方案一般是调整目录属主或权限chown -R nginx:nginx /data/www/my-app # 或者 chmod -R 755 /data/www/my-app第二种情况是目录下有文件但缺少index指令指定的索引文件访问目录时直接 403。前面配置里写了index index.html;如果目录里只有home.html没有index.htmlNginx 会认为无法索引而返回 403。有次某个活动页的部署脚本改了输出文件名把index.html改成了index.htm配置里没改线上全部 403好在那次是灰度环境及时发现没酿成事故。如果你用index index.html index.htm;这种写法就多了一个容错。还有一种 403 是访问规则触发的比如在location里写了deny all;这种是主动拒绝访问。这种场景下先检查配置文件里有没有访问控制相关指令。6.4 常见配置错误速查改完不生效、端口被占、变量拼写问题nginx -t报配置语法错误是最低级但也最容易遇到的问题。常见的语法错误包括忘记分号、花括号不配对、指令名拼写错误。Nginx 对指令名拼写很敏感个别不存在的指令会直接报unknown directive。配置改了但不生效先确认nginx -t通过并且执行了nginx -s reload。如果 reload 也执行了还不生效检查一下你改的文件是不是真的被include了。之前遇到一种反直觉的情况某个配置文件存在sites-available目录下但sites-enabled里的软链接过期了文件改来改去Nginx 根本没加载。排查方式就是看nginx -T输出里有没有你改的那个配置内容。端口被占用导致 Nginx 启动不了错误日志会提示address already in use。处理方式# 查看哪个进程占用了 80 端口 sudo lsof -i :80 # 或者 sudo netstat -tlnp | grep :80找到占用进程后要么停掉冲突服务要么改 Nginx 监听端口。有些云服务器平台默认会用 Apache 或者 HTTPD 占掉 80 端口如果有不认识的进程在监听可以在服务器安全组和进程列表里确认一下再处理。变量拼写问题极易被忽略。$host、$hostname、$server_name、$http_host这几个容易混。$host是请求行的域名或请求头中的Host字段小写形式$http_host是请求头里原始的Host保留原始大小写和端口$server_name是 Nginxserver块里配置的server_name值。做跳转或日志格式时选错变量输出结果和预期会差很多这类问题排查起来特别隐蔽因为不报错、只表现为行为不对。排查这类小问题我自己的经验是开启错误日志观察前后变化tail -f /var/log/nginx/error.log改配置、reload、发请求三步操作配一个日志观察窗口很快就能锁定问题方向。如果error.log也没给出线索就临时把日志级别调到debug虽然日志量会爆炸但排错效率能提上来。7. 一份生产级基础配置模板从零手写出可上线的完整站点前面把核心概念和配置项都拆开了现在把它们拼装成一个可以直接上线的完整配置。这个模板我在多个实际项目中用过涵盖了静态资源、路由回退、反向代理、安全头、Gzip、日志、HTTP 跳 HTTPS 这些前端 Web 服务的高频需求你可以直接抄过去改参数。server { listen 443 ssl http2; server_name www.example.com; # SSL 证书 ssl_certificate /etc/nginx/ssl/www.example.com.pem; ssl_certificate_key /etc/nginx/ssl/www.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; # 站点根目录 root /data/www/web; index index.html; # Gzip 压缩 gzip on; gzip_comp_level 5; gzip_min_length 1k; gzip_types text/plain text/css application/json application/javascript application/xml image/svgxml; gzip_vary on; # 安全响应头 add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header X-XSS-Protection 1; modeblock; # 静态资源缓存 location ^~ /assets/ { expires 7d; add_header Cache-Control public; } # 图片缓存 location ~* \.(png|jpg|jpeg|gif|ico|webp)$ { expires 30d; add_header Cache-Control public; } # API 反向代理 location /api/ { proxy_pass http://backend_pool; 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 10s; proxy_read_timeout 60s; } # SPA 路由回退 location / { try_files $uri $uri/ /index.html; } # 日志 access_log /var/log/nginx/web_access.log; error_log /var/log/nginx/web_error.log warn; } # HTTP 跳转 HTTPS server { listen 80; server_name www.example.com; return 301 https://$host$request_uri; }对照这个模板我解释几个配置思路根路径用root /data/www/web和try_files $uri $uri/ /index.html承接 SPA 路由回退前端路由和历史模式刷新都不会有 404 问题。/assets/是构建工具输出的静态资源目录带^~做前缀匹配优先处理设置 7 天浏览器缓存图片类型单独用正则匹配给 30 天的缓存时间。/api/专门做反向代理proxy_set_header把真实客户端信息透传给后端超时时间根据后端接口耗时调整。location /作为兜底处理所有未被上述规则捕获的路径回退到index.html。上线前要做三个动作第一nginx -t验证语法第二curl -I测试首页和静态资源的响应头是否正确第三检查访问日志确认请求分布符合预期。上面模板里的域名为示例实际部署时替换成自己的域名、证书路径和站点目录即可。8. 一次真实的部署排查实战从 404 到 502一小时内理清全链路结合 Config 模板再分享一次完整的部署排查过程。有次帮一个活动页项目做上线前检查前端同学说测试环境部署完页面打不开报 404。我按部就班登录服务器用curl -v http://test.example.com/看到实际响应是 404然后查error.log发现 Nginx 在尝试访问/data/www/activity/index.html时找不到文件。到服务器上一看目录结构只部署到/data/www/activity_dist压根没有/data/www/activity。运维的部署脚本把构建产物解压到了错误的位置Nginx 配置里root指向的目录和实际部署目录不一致。这个问题的根源不在 Nginx而是部署流程里路径约定没统一。把root改成/data/www/activity_dist后页面正常打开。页面是能打开了接着新的问题又来了登录接口 502。用curl http://127.0.0.1:8080/api/login从服务器本机请求返回正常但访问test.example.com/api/login显示 502。看error.log里面写着connect() failed (111: Connection refused) while connecting to upstream。这就奇怪了Nginx 配置里proxy_pass http://backend_poolupstream写的是server 127.0.0.1:8080本机明明能通。后来发现nginx.conf里还有另一个server块占用了 8080 端口backend_pool里的 8080 被别的进程先抢占Nginx 转发过去的请求被那个进程拒绝了。把活动页的upstream端口从 8080 改成 8082 并对应启动后端实例后接口恢复。这次排错全程不到一小时工具只有curl、netstat、tail -f error.log三样没有任何高大上的手段。问题也不难但每一步都依赖对 Nginx 路径匹配和代理机制的理解。如果你对这些配置逻辑掌握得很清楚线上碰到问题后定位路径是清晰的不会东一榔头西一棒子。9. 我的一点实操体会回头看这篇文章里提到的内容从静态站点到反向代理再到 HTTPS、Gzip、日志和排错覆盖面不算广但每一条都是我在真实场景里验证过的。Nginx 的学习曲线并没有想象中陡峭它的难点不在于命令和参数本身而在于当你面对一个线上问题能否快速把请求链路拆解清楚浏览器到 Nginx 的路径匹配是否命中预期Nginx 到后端的代理拼接是否产生偏差超时、缓存、日志这几个环节有没有隐藏问题。另外想强调一个我在实际工作中反复确认的心得为 Nginx 的配置留好版本记录是个好习惯。我见过好几个项目线上 Nginx 配置改来改去没有做记录出问题之后连最近谁改了什么都说不清。我自己后来把每个站点的配置文件纳入管理每次改动都同步到配置仓库并且写清改动原因。这个习惯在排查问题时帮了大忙配合nginx -T查看解析后的实际配置能在几分钟内确认线上配置和预期是否一致。如果你正在学习这个方向我建议自己去本地环境搭一遍完整的流程装 Nginx、配置一个静态站点、把 Vite 构建的 SPA 部署上去、加一层 API 反向代理、再配一个 HTTPS 证书。按这篇文章的步骤走一遍把每个配置项都亲手敲进去把location匹配规则和proxy_pass斜杠差异用curl实测一遍你对 Nginx 的理解会远超只看文档的效果。毕竟这一层服务能不能稳稳兜住你的前端应用只有自己上手跑过才知道。
阅读完成 · 觉得有帮助?