nginx 入门:配置文件结构、location 匹配规则与反向代理
nginx 的配置文件看起来像一门小语言:一堆花括号嵌套,指令名字都很直白,照着网上抄一段通常也能跑起来。麻烦的是抄来的配置一改就出问题——多加一个斜杠行为完全变了,多写一个 location 块请求就打到别处去了。这些不是玄学,是 nginx 有一套明确的匹配规则,只是很多教程没讲。
本文所有指令行为都对照 nginx 官方文档核对过。当前开源版本是 mainline 1.31.4、stable 1.30.4,下面的配置在这两条线上都适用。
nginx 是什么:一个 master 带一群 worker
nginx 是一个 HTTP 服务器兼反向代理。它跟传统 Web 服务器最大的区别在进程模型:不是”一个连接开一个线程”,而是少量 worker 进程,每个 worker 用事件驱动的方式同时处理成千上万个连接。
启动之后进程长这样:
1 | ps -ef | grep nginx |
- master 进程:以 root 身份运行,负责读配置、绑定端口(绑 80/443 这种低端口需要特权)、管理 worker 的生死。它不处理任何请求。
- worker 进程:以低权限用户运行,真正处理请求。数量由
worker_processes决定,一般设成auto,即等于 CPU 核数。
这个分工直接决定了 nginx -s reload 为什么能做到不断连接:master 收到重载信号后,用新配置拉起一批新 worker,同时通知老 worker “别接新连接了,把手上的处理完就退出”。所以重载期间已经建立的连接不会被打断。
flowchart TD
M["master 进程<br/>root 身份,只管配置和 worker"] --> W1["worker 1<br/>处理连接"]
M --> W2["worker 2<br/>处理连接"]
M --> W3["worker 3<br/>处理连接"]
R["nginx -s reload"] -.->|"发信号"| M
M -.->|"用新配置拉起"| WN["新 worker"]
M -.->|"处理完手头请求后退出"| W1
配置文件的层级:块套块,指令会继承
主配置文件通常在 /etc/nginx/nginx.conf。它的结构是几层嵌套的块(官方叫 context),从外到内是 main、events、http、server、location:
1 | # 最外层叫 main context,不在任何花括号里 |
有两条规则贯穿整个配置文件:
- 指令有它能出现的位置。
worker_connections只能写在events里,location只能写在server或另一个location里。官方文档每个指令都标了 Context,写之前查一下能省很多事。 - 内层没写的指令,从外层继承。
http里写了sendfile on,所有server和location都继承这个值;某个location里再写sendfile off,只覆盖它自己那一块。
一个请求进来之后,nginx 怎么找到对应的配置
处理流程分成两步,先选 server,再在这个 server 里选 location。
flowchart TD
A["请求到达 80 端口"] --> B["按 Host 头找 server"]
B -->|"Host 匹配上某个 server_name"| C["用那个 server 块"]
B -->|"没有 Host 头,或者一个都匹配不上"| D["用这个端口的默认 server"]
C --> E["在选中的 server 里挑 location"]
D --> E
E --> F["执行 location 里的指令<br/>返回文件 / 转发给后端 / 返回状态码"]
用一个具体例子走一遍这个流程。假设配置里有这两个 server 块,都监听 80 端口:
1 | server { |
现在有个请求打进来,浏览器访问的是 http://shop.example.com/index.html。nginx 处理这个请求分两步走:
- 先选 server:请求里带着
Host: shop.example.com这个头,nginx 拿它去跟每个server块的server_name比对。第一个块的server_name是blog.example.com,对不上;第二个块是shop.example.com,对上了。于是这个请求归第二个server块管。 - 再选 location:进入这个
server块之后,nginx 看它里面有哪些location。这里只有一个location /,请求路径/index.html自然匹配它。
最终结果:nginx 去 /var/www/shop 这个目录下找 index.html 返回给浏览器。
如果换一个请求,Host 头写的是 admin.example.com——这两个 server 块都没有声明这个域名,谁都匹配不上。这种情况下请求会被扔给这个端口的默认 server,也就是配置里 80 端口对应的第一个 server 块(这里是 blog.example.com 那个),除非专门指定了别的默认块。
默认 server 是监听端口的属性,不是域名的属性。同一个端口上如果没有任何 server 块显式写了 default_server,nginx 就把配置里该端口的第一个 server 块当默认。
1 | server { |
线上环境值得专门写这么一个默认 server 兜底。否则别人把域名解析到你的 IP,请求会落进你的第一个 server 块,等于白蹭你的站点。
location 匹配优先级:先比前缀,再比正则
这是 nginx 配置里最容易出错的地方,也是必须记住的一套规则。location 有四种写法:
1 | location = /exact { } # = 精确匹配:URI 必须完全等于 /exact |
nginx 的匹配顺序是这样的:
- 先在所有前缀 location(没修饰符的、
^~的、=的)里找。=精确命中就直接用它,搜索结束。 - 否则记住匹配长度最长的那个前缀 location(注意是最长,不是配置里写在最前面的)。
- 如果记住的那个前缀带
^~,直接用它,跳过正则检查。 - 否则按配置文件里的书写顺序逐个试正则,第一个命中的正则 location 生效,后面的不再看。
- 所有正则都不命中,才回过头用第 2 步记住的那个前缀 location。
关键点有两个:前缀 location 之间比的是长度,跟书写顺序无关;正则 location 之间比的是书写顺序,跟长度无关。
官方文档的例子最能说明问题:
1 | location = / { |
各个请求分别命中哪个:
| 请求 URI | 命中 | 为什么是它,不是别的 |
|---|---|---|
/ |
A | A 是精确匹配,一旦对上直接用,连别的 location 都不用比了 |
/index.html |
B | 前缀里只有 / 对得上,而且没有正则能匹配 .html 结尾,所以停在 B |
/documents/document.html |
C | 前缀里 /documents/ 比 / 更长,优先用长的;正则里也没有能匹配它的,所以停在 C |
/images/1.gif |
D | 前缀里最长的是 /images/,而且它带 ^~——带了这个记号就不再看正则,E 直接被跳过 |
/documents/1.jpg |
E | 前缀里最长的是 /documents/,但它不带 ^~,所以还要接着比正则,正则命中了 E |
最后两行放一起最能看出 ^~ 的作用:同样是一张 jpg 图片,放在 /images/ 下命中的是 D,放在 /documents/ 下命中的却是 E。区别就在于 /images/ 这个前缀带了 ^~——带上它,等于告诉 nginx“这个目录下的东西认前缀就行,不用再去比正则”。静态资源目录经常这么写,就是为了不被后面那些用来处理图片、脚本的正则规则半路截胡。
proxy_pass 后面带不带路径,行为完全不同
反向代理是 nginx 用得最多的功能,而 proxy_pass 的这个差异是最常见的翻车点。
规则只有一句:代理地址后面带了 URI(哪怕只是一个斜杠),nginx 就会把 location 匹配掉的那一段替换成这个 URI;不带 URI,则原样转发完整路径。
假设请求是 /api/users/1:
1 | # 写法一:不带 URI —— 原样转发 |
写法一和写法二只差一个斜杠,后端拿到的路径就差了一个 /api 前缀。后端框架如果按 /users/1 注册路由,用写法一就会 404;后端如果本身就带 /api 前缀,用写法二又会 404。
这里还有一条相关的规则:前缀 location 以斜杠结尾、且里面用了 proxy_pass,那么访问不带尾斜杠的地址时,nginx 会返回 301 重定向到带斜杠的版本。也就是说上面的配置里,请求 /api 会被 301 到 /api/。不想要这个重定向,得单独加一个精确匹配的 location:
1 | location /api/ { |
还有一点要注意:location 用正则写的时候,proxy_pass 不能带 URI。因为”被匹配掉的那一段”在正则场景下没法确定,nginx 会直接在启动时报配置错误。
一份能用的反向代理配置
只写 proxy_pass 的话,后端拿不到真实客户端信息,得把几个头补上:
1 | upstream backend { |
代理 WebSocket 需要额外两个头,因为 WebSocket 靠 HTTP 的 Upgrade 机制握手:
1 | location /ws/ { |
root 和 alias:一个是拼接,一个是替换
这两个指令都是指定文件在磁盘上的位置,区别在于怎么和请求路径组合。
root 是把完整的请求 URI 拼在后面:
1 | location /i/ { |
alias 是把 location 匹配掉的那一段替换成它的值:
1 | location /i/ { |
判断该用哪个的方法:磁盘目录结构和 URL 路径结构一致,用 root;两者对不上、需要做一次路径改写,用 alias。官方文档也建议,如果 location 的路径正好是目录路径的最后一段(比如 location /images/ 配 /data/w3/images/),改用 root /data/w3 更清晰。
用 alias 有个约定要遵守:location 以斜杠结尾,alias 的值也要以斜杠结尾,两边不一致会拼出意料之外的路径。
单页应用的经典配置
前端打包出来的 SPA,路由是前端接管的,刷新任何子路径都得返回同一个 index.html:
1 | location / { |
静态资源、压缩和缓存
1 | http { |
HTTPS 配置
证书拿到之后,配置本身很简单:
1 | server { |
证书链拼不完整是个常见问题:浏览器通常自带中间证书所以看着正常,但 curl 和很多服务端 HTTP 客户端会直接报证书校验失败。ssl_certificate 指向的文件必须是站点证书在前、中间证书在后拼起来的那一个。
日常操作的几条命令
1 | nginx -t |
改配置的固定动作是 nginx -t && nginx -s reload,用 && 串起来,语法不过就不会执行重载。
日志方面,默认的 combined 格式不带响应时间,排查慢请求时基本没用,建议自定义一个:
1 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' |
$request_time 明显大于 $upstream_response_time 时,说明时间花在了 nginx 和客户端之间——通常是客户端网络慢,或者响应体太大传输耗时。排查线上问题时这个区分能省掉大量猜测。想进一步看连接层面的状态,可以配合 ss 和 lsof 一起用。










