Nginx

简介

  • Nginx(读作 “engine x”)是一个高性能的 HTTP 与反向代理 Web 服务器,同时也能当 IMAP/POP3/SMTP 代理。它由伊戈尔·赛索耶夫(Igor Sysoev)为俄罗斯访问量第二的 Rambler.ru 站点开发,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日——这两条日期在本机 docs/CHANGES 里能直接查到:文件最后一条就是 Changes with nginx 0.1.0 04 Oct 2004,开头那句 The first public version.。
  • 它以类 BSD 许可证发布。本机 docs/LICENSE 原文是两条条件的 BSD-2-Clause,版权行写着 Copyright (C) 2002-2021 Igor Sysoev 与 Copyright (C) 2011-2025 Nginx, Inc.。
  • 原笔记里那句「2011 年 6 月 1 日,nginx 1.0.4 发布」是对的,CHANGES 里 Changes with nginx 1.0.4 01 Jun 2011 一字不差;顺带能看出 1.0.4 那次改的就是本文后面要用的东西——「map 指令里正则的大小写敏感性改用 ~ / ~* 前缀表示」。
  • 它出名的原因是:内存占用小、并发能力强、配置直观。官方对自身的定位(nginx.org 首页)是「One master and several worker processes」,即一个 master 进程带多个 worker 进程,worker 以非特权用户运行——这个结构决定了后面「reload 不断连接」「改配置只换 worker」这些行为。
  • 官网在 2026-11-03 实测给出的两条版本线是 Mainline 1.31.8 与 Stable 1.30.6;而 Ubuntu 26.04 仓库里能 apt 装到的是 1.28.3(CHANGES 顶部:Changes with nginx 1.28.3 24 Mar 2026,第一条是修 alias + WebDAV COPY/MOVE 的缓冲区溢出)。也就是说:发行版仓库的版本永远比官网旧一档,追新要自己编译。
  • 本文的实测环境:WSL2 的 Ubuntu 26.04.1,nginx 1.28.3-2ubuntu1.12(built with OpenSSL 3.5.5);另外在 Windows 侧解压跑了一份官方 nginx-1.28.3.zip 做对照。为了不干扰系统里那个监听 80 端口的正式实例,测试用的是独立配置文件 + 独立端口:8088/8089(路由与反向代理)、8090/8091/8443(静态、return 444、HTTPS)、8092~8097(server_name、鉴权、缓存、rewrite、proxy_pass 路径规则)、8112/8113(return 与静态文件对照、正向代理)。所有结论后面都会附真实输出。

正向代理

  • 概述

    我是一个用户,我访问不了某网站,但我能访问一台代理服务器;这台代理能访问那个网站,于是我连上代理并告诉它「我要这个地址的内容」,它替我去取,再返回给我。客户端显式地把目标地址交给代理,这是正向代理的本质。

  • 概念

    正向代理是位于客户端与原始服务器(origin server)之间的一台服务器。客户端向代理发请求并指定目标,代理把请求转交给目标服务器,再把拿到的内容回给客户端。客户端知道自己真正访问的是谁,但目标服务器只看到代理的 IP。

  • 用途

    1. 给防火墙内的局域网客户端提供访问 Internet 的出口(公司/学校的上网代理)。
    2. 用缓冲(缓存)特性减少重复的对外带宽消耗。
    3. 隐藏客户端真实身份,或让多台内网机器共用一个公网出口。
  • 图解

  • 用 nginx 搭一个正向代理(实测)

    nginx 默认是「按 server_name 找虚拟主机」,而正向代理的请求行里带的是完整 URL,所以要靠 $host/$http_host 变量把目标地址取出来再转发。最小可用配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # /etc/nginx/nginx.conf 的 http 段里
    resolver 10.255.255.254 valid=5s ipv6=off; # 目标域名要靠它解析

    server {
    listen 8888;

    location / {
    proxy_pass http://$http_host; # 注意是 $http_host 不是 $host
    proxy_set_header Host $http_host;
    }
    }

    客户端这样用:

    1
    curl -s -x http://127.0.0.1:8888 http://127.0.0.1:8101/hdrs
    1
    2
    GET /hdrs HTTP/1.0
    Host: 127.0.0.1:8101
    • $host 会把端口丢掉,这点实测很坑。第一版配置我写的是 proxy_pass http://$host;,结果 http://127.0.0.1:8101/hdrs 被转到了 80 端口,客户端拿到的是另一台 nginx 的 404 页面,而不是 8101 后端的响应。换成 $http_host(保留 主机:端口 原样)才正确。
    • 目标写成域名时必须配 resolver。proxy_pass 里一旦带变量,nginx 就不再在启动时解析域名,而是运行时查 resolver;不配就直接报 no resolver defined to resolve xxx。本机配了 resolver 10.255.255.254(WSL 的 /etc/resolv.conf 里那个地址)之后,curl -x ... http://localhost:8101/hdrs 才通,上游收到的 Host 是 localhost:8101。
    • 千万别把正向代理挂在对外端口上还不加访问控制。它本质是一个「任意地址转发器」,公网上的开放正向代理会被拿去做扫描、洗流量、绕过 IP 黑名单。生产上要么 allow/deny 限内网,要么加 auth_basic,要么只监听 127.0.0.1。
    • 还有个反直觉的实测结果:这台代理不经过 -x 直接被访问时,$http_host 就是它自己(127.0.0.1:8113),于是它把请求转给自己,瞬间形成回环,返回 500,error.log 里打的是
      1
      [alert] 64 worker_connections are not enough while connecting to upstream
      因为每一跳都会占掉一个连接,很快就撞到 worker_connections 上限。这条报错原文后面「常见报错」一节还会再见到。

反向代理

  • 概念

    反向代理是替服务器接收请求的那一层:客户端以为自己访问的就是站点本身,实际上请求被 nginx 收下,转给内网的真实服务,再把响应原路返回。对外表现为一个站点,对内可以是一台或多台机器。

  • 用途

    1. 把防火墙后面的服务器暴露给 Internet 用户访问,后端机器不需要公网地址。
    2. 为后端多台机器做负载均衡;或者给慢后端做缓冲,让慢服务不至于拖垮整个站点。
    3. 启用高级 URL 策略:让分布在不同后端系统上的页面,对外呈现成同一个 URL 空间。
    4. 统一做 TLS 终结、HTTP/2、压缩、缓存、鉴权、限流——这些活儿不必每个后端各实现一遍。
  • 图解

  • 最小可用配置(实测)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 后端:本机 127.0.0.1:8101(一个 Python http.server)
    server {
    listen 8089;
    server_name proxy.local;

    location / {
    proxy_pass http://127.0.0.1:8101;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    }
    1
    curl -s http://127.0.0.1:8089/hdrs
    1
    2
    3
    4
    5
    6
    7
    GET /hdrs/hdrs HTTP/1.0
    Host: 127.0.0.1
    X-Real-IP: 127.0.0.1
    X-Forwarded-For: 127.0.0.1
    X-Forwarded-Proto: http
    User-Agent: curl/8.18.0
    Connection: close
  • 不写 proxy_set_header 时,上游到底看到什么(实测)

    原笔记说「默认情况反向代理不转发请求中的 Host 头部」,准确说法是:nginx 会把 Host 改写成 $proxy_host(也就是 proxy_pass 里那台上游的地址),而不是「不发」。把上面那四条 proxy_set_header 全删掉再打同一个后端:

    1
    curl -s http://127.0.0.1:8089/nohost/hdrs
    1
    2
    3
    4
    5
    6
    GET /nohost/hdrs HTTP/1.0
    Host: 127.0.0.1:8101 # 变成上游地址了
    X-Real-IP: None # 这三个头默认根本不存在
    X-Forwarded-For: None
    X-Forwarded-Proto: None
    Connection: close
    • 这就是为什么基于域名路由的后端(Django 的 ALLOWED_HOSTS、虚拟主机、request.get_host())必须显式设置 proxy_set_header Host $host;,否则后端看到的 Host 是内网地址。
    • nginx 到上游默认用 HTTP/1.0 + Connection: close,即使客户端用的是 1.1 长连接。实测客户端发 HTTP/1.1、Connection: keep-alive,上游收到的请求行仍是 GET /hdrs/hdrs HTTP/1.0、Connection: close。要改成 1.1 得写 proxy_http_version 1.1; 并把 Connection 置空:
      1
      2
      3
      4
      5
      location /http11/ {
      proxy_pass http://127.0.0.1:8101;
      proxy_http_version 1.1;
      proxy_set_header Connection "";
      }
      1
      GET /http11/hdrs HTTP/1.1
      WebSocket 转发必须这么写,原因见「WebSocket 转发」一节。
    • 客户端 IP 默认不会传给后端,X-Real-IP、X-Forwarded-For 都得自己加;后端要拿真实 IP,还得配合 real_ip 模块(本机这份 nginx 编译时带了 --with-http_realip_module,可以直接用 set_real_ip_from + real_ip_header)。

负载均衡

  • 概述

    负载均衡是把多台服务器组成一个对称的集合,每台地位等价、都能单独对外提供服务;再通过某种分配算法把外部请求分给其中一台,由它独立回应客户端。nginx 里这件事由 upstream 模块做:先声明一组后端,再在 proxy_pass 里按名字调用。

  • 作用

    均摊客户请求,解决大量并发访问问题;顺带提供「后端挂了自动摘除」的能力,用户访问不受单台故障影响。

  • 四种调度算法(原笔记的说法要更正)

    1. 轮询(默认)
      每个请求按时间顺序逐一分配到不同后端;某台宕机时自动剔除。weight 参数指定权值,值越大被分到的概率越高,适合后端机器性能不均的情况。实测(两台无 weight,8 次请求):
      1
      2
      4 backend 8101 /rr2/x
      4 backend 8102 /rr2/x
      严格 1:1 交替。带 weight=3 与 weight=1 时,12 次请求实测 10:2——接近 3:1 但不是精确 3:1,因为调度是平滑加权的,样本小的时候会偏。
    2. ip_hash
      按客户端 IP 的哈希结果分配,同一个 IP 固定落到同一台后端,能缓解动态页面的 session 共享问题(前提是这个 IP 真的稳定,见下面那条注意)。实测同一客户端 6 次、10 次请求全部落在 8101:
      1
      6 backend 8101 /rr/x
    3. hash(即原笔记说的 url_hash)
      按访问 URL 的哈希分配,让同一个 URL 定向到同一台后端,提高后端缓存命中率。原笔记说「Nginx 本身不支持 url_hash,必须安装 hash 软件包」已经过时:hash 指令从 1.7.2 起就是内置的,写法是 hash $request_uri;,不需要任何额外软件包。实测三个不同 URL 各自稳定:
      1
      2
      3
      5 backend 8102 /urihash/a
      5 backend 8101 /urihash/b
      5 backend 8102 /urihash/c
      另外 hash 还支持 consistent 参数(一致性哈希)。实测 hash $remote_addr consistent 把同一个客户端映射到了 8102,而 ip_hash 映射到的是 8101——两者算法不同,落点也不同,别以为 consistent 只是「加个开关」。
    4. fair
      按后端响应时间分配,响应快的优先。这条确实不是 nginx 内置的,它是第三方模块(upstream_fair),原笔记这点没错。内置的近似替代品是 least_conn(优先选当前连接数最少的那台)和 random。实测 least_conn 串行 6 次是 3:3;random two 8 次全落 8101——因为后端响应太快(毫秒级),「随机挑两台里负载低的」这一层根本区分不出来。
  • 状态与容错参数

    参数 含义 实测/说明
    down 当前 server 暂时不参与负载均衡 配合 backup 用:把主用机器标 down 时流量立刻切到 backup
    backup 备份机器,其他非 backup 机器全故障或全忙时才启用,压力最轻 实测主用在线时 5 次请求 一次都没走 backup
    max_fails 允许请求失败的次数,默认 1;超过后按 proxy_next_upstream 的规则处理 默认值就是 1,很多人以为要显式写
    fail_timeout 经历 max_fails 次失败后暂停服务的时间,默认 10s;与 max_fails 配合使用 实测被标记不可用期间,请求直接走健康的那台,客户端仍是 200
    weight 权值,默认 1 见上面 10:2 的实测
    max_conns 该后端的最大并发连接数,默认 0(不限) 本机后端响应太快,4 路并发测不出效果(全 200),生产上要配合慢后端才看得出
  • 一条重要注意

    • 原笔记写「当负载调度算法为 ip_hash 时,后端服务器在负载均衡调度中的状态不能是 weight 和 backup」。实测这条只对了一半:
      1
      2
      3
      ip_hash + weight=2  ->  nginx -t 通过(weight 被静默忽略,不参与分配)
      ip_hash + backup -> [emerg] balancing method does not support parameter "backup"
      ip_hash + down -> nginx -t 通过,且该机确实不参与分配
      也就是说 backup 是硬报错,weight 是软忽略——后者更危险,因为它不告诉你权值没生效。
    • 同一个 upstream 里写两种算法(ip_hash; least_conn;)时,nginx -t 也过,但启动会打
      1
      [warn] load balancing method redefined in /etc/nginx/nginx.conf:6
      并以最后写的那个为准。实测这样配下来 4 次请求全落 8102(因为另一台标了 down),ip_hash 完全没起作用。
    • 更实用的一条是:ip_hash 依赖客户端 IP,而真实互联网用户经常在小区间跳变(移动网络、公司出口 NAT),「同一 IP = 同一用户」这个前提并不牢靠;要真正的会话保持,应该把 session 放到 Redis 之类的共享存储里,而不是指望 ip_hash。
  • 完整示例(本机实测通过的写法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    upstream suyin {
    # ip_hash; # 开了 ip_hash 就不能再给 server 写 weight/backup
    server 127.0.0.1:8101 weight=3 max_fails=1 fail_timeout=30s;
    server 127.0.0.1:8102 max_fails=1 fail_timeout=30s;
    server 127.0.0.1:8103 backup;
    }

    server {
    listen 8089;
    location / {
    proxy_pass http://suyin;
    }
    }
    • 原笔记这段里有一批 OCR 造成的拼写错误:welght=2、max_falls=1、fall_timeout=300s,以及「负债负载均衡」应为「负责负载均衡」;末尾还多出一个右花括号(})导致结构不配对。这类拼写错误 nginx 不会逐个提示,nginx -t 只会报第一个致命错误,所以抄完务必跑一次 -t。
  • Ubuntu / Debian(发行版仓库,本机实测走的就是这条路)

    1
    2
    3
    4
    sudo apt-get update
    apt-cache policy nginx | head -3
    sudo apt-get install -y nginx
    systemctl status nginx --no-pager | head -5
    1
    2
    3
    4
    5
    nginx:
    Installed: (none)
    Candidate: 1.28.3-2ubuntu1.12
    ● nginx.service - A high performance web server and a reverse proxy server
    Active: active (running)
    • 装完立刻就是一个能用的服务:active + enabled(开机自启),默认站点监听 80,ss -ltnp 能看到 80 由 nginx 占用。想另起测试实例时要么先 systemctl stop nginx,要么像本文这样用独立配置文件避开 80。
    • nginx.org 官方 apt 源在 Ubuntu 26.04 上目前没有发布线。照网上常见的写法加源:
      1
      2
      3
      echo 'deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/ubuntu resolute nginx' \
      | sudo tee /etc/apt/sources.list.d/nginx.list
      sudo apt-get update
      实测报错:
      1
      E: The repository 'http://nginx.org/ubuntu resolute Release' does not have a Release file.
      所以这台机器上「apt 装最新稳定版」只能拿到 1.28.3;要 1.30/1.31 得自己编译(见下一个页签)。
    • 布局是 Debian 风格而不是官方包风格,ls /etc/nginx 实测有:conf.d/、sites-available/、sites-enabled/、modules-available/、modules-enabled/、snippets/、mime.types、proxy_params、fastcgi_params、scgi_params、uwsgi_params。本机实测 conf.d/ 和 modules-enabled/ 都是空的,snippets/ 里有 fastcgi-php.conf 与 snakeoil.conf 两个现成片段。
  • RHEL / CentOS / Rocky:sudo dnf install nginx,主配置在 /etc/nginx/nginx.conf,站点目录约定不同(没有 sites-enabled,用 conf.d/*.conf)。

  • macOS:brew install nginx,默认前缀在 /opt/homebrew/(Apple Silicon)或 /usr/local/(Intel),且 brew 版监听的是 8080(非 root 不能绑 1024 以下端口)。这条本机未实测,只作为常见做法记录。

  • 源码编译能装到官网任意版本,也能自选编译哪些模块。本机实测过程:

    1
    2
    3
    4
    sudo apt-get install -y build-essential zlib1g-dev libpcre2-dev libssl-dev
    wget https://nginx.org/download/nginx-1.28.3.tar.gz
    tar -xzf nginx-1.28.3.tar.gz
    cd nginx-1.28.3
    • 解压参数是 -z 不是 -Z。原笔记写的 tar -xZvf nginx-1.11.3.tar.gz 里 -Z 对应的是老的 .Z(compress)格式,对 .tar.gz 会直接失败;正确写法是 tar -xzvf 或 tar -xzf。
    • 原笔记那句 tar -xZvf pcre- -841.tar.gz 除了同样的 -Z 问题,文件名中间还多了一个空格和多余的 -,本来就不可能存在这样的文件。
  • 配置(本机实测通过的参数组合):

    1
    2
    3
    4
    5
    6
    7
    ./configure --prefix=/usr/local/nginx \
    --with-http_ssl_module \
    --with-http_v2_module \
    --with-http_realip_module \
    --with-http_stub_status_module \
    --with-http_gzip_static_module \
    --with-threads
    • 原笔记里 --with-pcre=../pcre-8.41 --with-openssl=../openssl-1.0.1 这种「把第三方库源码一起编进来」的写法本身没错,但现在不该这么用了:openssl-1.0.1 早在 2016 年就 EOL(有已知漏洞),pcre-8.41 也已停维护;nginx 1.21.5 起 PCRE 换成了 PCRE2,--with-pcre= 指向 PCRE1 源码目录会直接报错。正确做法是用系统的开发包(libpcre2-dev、libssl-dev),让 configure 自己找。
    • 原笔记说「如果出现 pcre.h No such file or directory,安装 libpcre3 libpcre3-dev」——这条只对 1.21.5 之前的老 nginx 成立;新版要装的是 libpcre2-dev,报错文件名也变成 pcre2.h。
  • 编译与安装:

    1
    2
    make -j8
    sudo make install
    • 本机实测:configure + make -j8 + make install 三步在 8 核 WSL 上不到一分钟就跑完(/usr/local/nginx 里四个子目录的时间戳都在同一分钟内),编译期 -Werror 打开也没有任何警告。装完 /usr/local/nginx 只占 5.2M,源码树 /usr/local/src/nginx-1.28.3 占 26M。
    • make install 的日志很罗嗦,全是一串 test -f '...' || cp ...:它会把 nginx.conf、mime.types、fastcgi_params、fastcgi.conf、scgi_params、uwsgi_params 各拷一份同名的 .default 文件留作参照,再顺手 mkdir -p logs。所以升级配置时想恢复出厂设置,直接看 *.default。
  • 装完的目录结构(--prefix=/usr/local/nginx,实测):

    1
    2
    3
    4
    conf/   配置文件(nginx.conf、mime.types、fastcgi_params、uwsgi_params…外加同名的 .default 参照)
    html/ 静态网页(index.html、50x.html)
    logs/ 日志文件(access.log、error.log、nginx.pid)
    sbin/ 可执行文件(nginx,约 5.1M)
    • 原笔记这段是对的,只有一个小问题:源码版不会自带 systemd 单元,systemctl start nginx 找不到服务,得自己写一个(写法可参考《Linux 系统服务》那篇)。
  • 验证:

    1
    2
    /usr/local/nginx/sbin/nginx -V
    sudo /usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
    1
    2
    3
    4
    5
    6
    7
    nginx version: nginx/1.28.3
    built by gcc 15.2.0 (Ubuntu 15.2.0-16ubuntu1)
    built with OpenSSL 3.5.5 27 Jan 2026
    TLS SNI support enabled
    configure arguments: --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module
    --with-http_realip_module --with-http_stub_status_module --with-http_gzip_static_module
    --with-threads
    • 和发行版那份对比就能看出取舍:源码版只编了你要的模块(这里就没有 http_v3、stream、dav、sub),而 apt 那份几乎全编上了、还带 --with-debug。想知道某个指令为什么「不认识」,nginx -V 一比对就明白。
  • 官方直接提供 Windows 二进制,解压即用,适合本地试配置;不适合压测和生产。本机实测:

    1
    2
    3
    4
    # 下载 https://nginx.org/download/nginx-1.28.3.zip(实测 2,743,427 字节,44 个条目)
    Expand-Archive nginx-win.zip -DestinationPath .
    cd nginx-1.28.3
    .\nginx.exe -V
    1
    2
    3
    4
    5
    6
    7
    8
    9
    nginx version: nginx/1.28.3
    built by cl 19.44.35217 for x86
    built with OpenSSL 3.5.5 27 Jan 2026
    configure arguments: --with-cc=cl --builddir=objs.msvc8 --with-debug --prefix=
    --conf-path=conf/nginx.conf --pid-path=logs/nginx.pid --http-log-path=logs/access.log
    --error-log-path=logs/error.log --sbin-path=nginx.exe
    --http-client-body-temp-path=temp/client_body_temp --http-proxy-temp-path=temp/proxy_temp
    ... --with-cc-opt=-DFD_SETSIZE=1024 --with-pcre=objs.msvc8/lib/pcre2-10.47
    --with-zlib=objs.msvc8/lib/zlib-1.3.2 --with-openssl=objs.msvc8/lib/openssl-3.5.5 ...
    • 注意 built by cl ... for x86:官方 Windows 包是 32 位构建;--prefix= 是空的,所以必须先 cd 到解压目录或用 -p 指定前缀,否则它找不到 conf/nginx.conf 和 logs/。
    • 少了什么:Windows 版没有 --with-http_v3_module(QUIC/HTTP/3)、没有 perl/xslt/image_filter/geoip 这些模块;--with-mail、--with-stream 倒是直接编进去了(Linux 版是 dynamic)。
    • -DFD_SETSIZE=1024 是硬伤:单进程能同时看护的套接字数被编译期写死在 1024,这就是「Windows 版并发上不去」的根源。
  • 启动与停止(实测):

    1
    2
    3
    .\nginx.exe -p . -c conf\nginx_win.conf      # 前台阻塞运行,Ctrl+C 或另开窗口发信号
    .\nginx.exe -s reload -p . -c conf\nginx_win.conf
    .\nginx.exe -s quit -p . -c conf\nginx_win.conf
    • 与 Linux 版不同,nginx.exe 启动后不转后台,命令行会一直占着那个窗口;窗口被关掉,服务就没了。要常驻可以用 start /B nginx.exe 或注册成服务(第三方 WinSW/NSSM)。
  • 实测的两条平台差异:

    1
    2
    3
    4
    2026/11/04 01:57:20 [notice] using the "select" event method
    2026/11/04 01:57:20 [notice] start worker processes
    2026/11/04 01:57:20 [notice] start worker process 2392
    2026/11/04 01:57:20 [notice] start worker process 50412
    • 事件模型是 select,不是 Linux 上的 epoll;日志里每个 worker 还会打 create thread ...(Windows 版靠线程池做文件 IO)。
    • 网上流传的「Windows 版只能起一个 worker」在本版不成立:配置写 worker_processes 2,实测 Get-Process nginx 得到 3 个进程(1 master + 2 worker)。
    • reload 与 quit 信号都正常工作,实测 reload 后仍能 200,quit 之后进程数归 0。

相关命令

  • 命令行选项(本机 nginx -h 原文)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Usage: nginx [-?hvVtTq] [-s signal] [-p prefix]
    [-e filename] [-c filename] [-g directives]

    Options:
    -?,-h : this help
    -v : show version and exit
    -V : show version and configure options then exit
    -t : test configuration and exit
    -T : test configuration, dump it and exit
    -q : suppress non-error messages during configuration testing
    -s signal : send signal to a master process: stop, quit, reopen, reload
    -p prefix : set prefix path (default: /usr/share/nginx/)
    -e filename : set error log file (default: stderr)
    -c filename : set configuration file (default: /etc/nginx/nginx.conf)
    -g directives : set global directives out of configuration file
    • 注意 -s 后面只认四个信号:stop、quit、reopen、reload。原笔记把 -c 说成「置顶配置文件」,其实是「指定配置文件」。
    • -p 的默认值来自编译参数 --prefix:Ubuntu 包是 /usr/share/nginx/,源码装是 --prefix= 给的那个目录,Windows 版是空串(所以必须显式 -p)。
    • -e 默认 stderr——这解释了为什么用 nginx -t 时错误直接打在终端上。
  • 常用操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 语法检查(改配置的第一步,永远先跑这个)
    sudo nginx -t

    # 语法检查 + 把 include 展开后的完整配置打到标准输出(排查「到底加载了哪个文件」最好用)
    sudo nginx -T | less

    # 重载配置:master 不变,worker 换新
    sudo nginx -s reload

    # 重新打开日志文件(日志切割后必须发这个,否则继续往旧 inode 写)
    sudo nginx -s reopen

    # 优雅退出 / 立即退出
    sudo nginx -s quit
    sudo nginx -s stop

    # 用指定配置文件启动一个完全独立的实例(本文所有测试都这么做)
    sudo nginx -c /etc/nginx/nginx_test.conf

    # 不进后台,前台运行(容器里必须这样,见最后一条)
    sudo nginx -g 'daemon off;'
    • -t 的退出码:正常时 0,配置有问题时 1,且第一个致命错误只报一条。实测把一个 server 块写到 http 外面:
      1
      2
      nginx: [emerg] "server" directive is not allowed here in /tmp/ngx/bad.conf:7
      nginx: configuration file /tmp/ngx/bad.conf test failed
    • -s reload 只往终端打一行,然后退出:
      1
      2026/11/04 00:02:56 [notice] 21835#21835: signal process started
      真正的换血过程记在 error.log 里,实测 reload 前后 master pid 不变(21599 → 21599),worker pid 变了(21836 → 21985)——这正是「reload 不断现有连接」的实现方式。
    • 用 systemd 管理时优先 systemctl reload nginx、systemctl restart nginx,别混用 nginx -s:单元文件里 PIDFile= 指向的 pid 和 nginx -s 找的可能不是同一个实例(尤其你用了 -c 起过额外实例时)。
    • 容器里必须 daemon off,否则 master 一退出容器就结束:
      1
      CMD ["nginx", "-g", "daemon off;"]
  • 看端口、抢端口

    1
    2
    3
    4
    ss -ltnp | grep :80          # Linux 上正确写法
    sudo ss -ltnp 'sport = :80' # 精确匹配 80,不会把 8080 也带出来
    sudo fuser -k 80/tcp # 干掉占用 80 端口的进程
    sudo lsof -i :80 # 另一种查法
    • 原笔记的 netstat -ano | grep 80 是 Windows 的语法(-a -n -o 是 Windows netstat 的参数组合,Linux 的 netstat 没有 -o 这个含义)。Linux 上应该用 ss -ltnp:-l 只列监听、-t TCP、-n 不做名称解析、-p 带上进程信息。
    • net-tools(netstat)在新版 Ubuntu 里默认不装,apt install net-tools 才有;ss 属于 iproute2,是现成的。
    • fuser -k 80/tcp 来自 psmisc 包,本机实测装完可用。
    • 实测正式实例跑起来时 ss -ltnp 的输出:
      1
      LISTEN 0 511 0.0.0.0:80 users:(("nginx",pid=20968,...))
      第二列那个 511 是 backlog,和 worker_connections 不是一回事。
  • 版本与编译信息

    1
    2
    nginx -v      # 只要版本号
    nginx -V # 版本号 + 完整 configure 参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    nginx version: nginx/1.28.3 (Ubuntu)
    built by gcc 15.2.0 (Ubuntu)
    built with OpenSSL 3.5.5 27 Jan 2026
    TLS SNI support enabled
    configure arguments: --with-cc-opt='-g -O2 ...' --with-ld-opt=... --prefix=/usr/share/nginx
    --conf-path=/etc/nginx/nginx.conf --error-log-path=stderr --pid-path=/run/nginx.pid
    --http-client-body-temp-path=/var/lib/nginx/body --http-proxy-temp-path=/var/lib/nginx/proxy
    --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-scgi-temp-path=/var/lib/nginx/scgi
    --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --modules-path=/usr/lib/nginx/modules
    --with-compat --with-debug --with-http_ssl_module --with-http_v2_module --with-http_v3_module
    --with-http_realip_module --with-http_stub_status_module --with-http_gzip_static_module
    --with-http_sub_module --with-http_slice_module --with-http_addition_module
    --with-http_auth_request_module --with-http_dav_module --with-http_flv_module
    --with-http_mp4_module --with-http_gunzip_module --with-http_random_index_module
    --with-http_secure_link_module --with-pcre-jit
    --with-http_geoip_module=dynamic --with-http_image_filter_module=dynamic
    --with-http_perl_module=dynamic --with-http_xslt_module=dynamic
    --with-mail=dynamic --with-mail_ssl_module
    --with-stream=dynamic --with-stream_realip_module --with-stream_ssl_module
    --with-stream_ssl_preread_module --with-stream_geoip_module=dynamic --with-threads
    • nginx -V 是排查「为什么这个指令不认识」的第一现场:--with-stream=dynamic 意味着要用四层代理得先 apt install libnginx-mod-stream,否则配置里写 stream {} 会报 "stream" directive is not allowed here;--with-http_v3_module 在则说明支持 QUIC。
    • 那一串 --http-*-temp-path=/var/lib/nginx/... 是各类临时文件目录。看到 ... failed (13: Permission denied) 之类的报错时,先按这几个路径排查属主。

测试

  • 启动与首次访问

    源码安装版:

    1
    2
    sudo /usr/local/nginx/sbin/nginx            # 启动(默认后台)
    ps -ef | grep '[n]ginx' # 看进程
    1
    2
    3
    4
    root  20965  nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
    www-data 20968 nginx: worker process
    www-data 20969 nginx: worker process
    ...(`worker_processes auto` 在 8 核机器上正好 8 个 worker)
    • 浏览器里输入服务器 IP,看到 Welcome to nginx 页即成功。命令行验证更靠得住:
      1
      2
      curl -s -o /dev/null -w 'code=%{http_code} size=%{size_download}\n' http://127.0.0.1/
      curl -sI http://127.0.0.1/ | head -3
      1
      2
      3
      4
      code=200 size=615
      HTTP/1.1 200 OK
      Server: nginx/1.28.3 (Ubuntu)
      Content-Type: text/html
      • size=615 就是 /var/www/html/index.nginx-debian.html 的大小;Server 头里那个 (Ubuntu) 来自发行版配置的 server_tokens build;,下一节会讲它的三种取值。
  • 改完配置怎么确认生效

    1
    2
    sudo nginx -t && sudo nginx -s reload
    curl -s -D - -o /dev/null http://127.0.0.1:8088/exact | tr -d '\r'
    1
    2
    3
    4
    HTTP/1.1 200 OK
    Server: nginx/1.28.3
    Content-Type: text/plain
    Content-Length: 12
    • 排错顺序永远是「-t → reload → 打一次请求 → 看 error.log」。reload 成功不代表配置符合你的预期,只代表语法合法。
    • 如果 reload 后行为没变,九成是这三种情况之一:改的不是被 include 加载的那个文件(用 nginx -T 确认)、浏览器/中间层缓存、或者你其实起了两个实例(ps -ef | grep nginx 数一下 master)。
  • 一个能自证「reload 不断连接」的最小实验

    1
    2
    3
    4
    # 终端 A:持续打请求
    while :; do curl -s -o /dev/null -w '%{http_code} ' http://127.0.0.1:8088/exact; done
    # 终端 B:改配置并重载
    sudo nginx -s reload -c /etc/nginx/nginx_test.conf
    • 实测 reload 期间请求序列里没有出现失败,error.log 里只有旧 worker gracefully shutting down、新 worker start worker process 这类 notice。原理就是上一条说的:master 不动,旧 worker 处理完存量连接才退出。
  • 收尾清理(本机实测)

    1
    ss -ltn | grep -E ':(8088|8089|8090|8091|8443|8112|8113)' | wc -l   # 应为 0

配置

  • 配置文件大致结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    #user  nobody;
    worker_processes 1;
    error_log logs/error.log notice;
    #pid logs/nginx.pid;

    events {
    worker_connections 1024;
    }

    http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    #gzip on;

    upstream myproject {
    server 127.0.0.1:8101;
    server 127.0.0.1:8102;
    }

    server {
    listen 80;
    server_name localhost;

    location / {
    root html;
    index index.html index.htm;
    }
    }

    server {
    listen 8080;
    server_name other.example.com;
    # 另一个虚拟主机
    }
    }
    • 上面这份是源码安装自带的 /usr/local/nginx/conf/nginx.conf 的骨架(实测文件 2656 字节),原笔记画的结构就是它,只是被 OCR 弄坏了:worker_ processes1(应为 worker_processes 1;)、error log logs/error.log(应为 error_log)、pid 和 logs/nginx.pid; 被拆成两行、worker_ _rlimit_ nofile(多了一个下划线)、locate {}(应为 location {}),而且 http 段里第一个 server 的右花括号位置错了。
    • 结构规则只有三条:main 在最外层、events 与 http 是 main 的直接子块、upstream/server 在 http 里,location 在 server 里。写错层级时 nginx 报的就是那句 "server" directive is not allowed here。
    • stream(四层代理)与 mail 和 http 平级,本机这份 Ubuntu 包里它们是 =dynamic,默认没加载,所以直接写会报 "stream" directive is not allowed here。
  • 主要区域

    1. main(全局设置)
    2. events(工作模式与连接数上限)
    3. http(HTTP 服务,内含 upstream 与 server)
    4. server(虚拟主机)
    5. location(URL 匹配)
    6. upstream(后端服务器组)
  • 发行版实际加载了哪些文件(Ubuntu 26.04 实测)

    1
    2
    sudo nginx -T 2>/dev/null | grep -E 'configuration file|^# configuration prefix'
    grep -n include /etc/nginx/nginx.conf
    1
    2
    3
    4
    6:include /etc/nginx/modules-enabled/*.conf;
    60: include /etc/nginx/mime.types;
    78: include /etc/nginx/conf.d/*.conf;
    79: include /etc/nginx/sites-enabled/*;
    • 也就是说 Ubuntu 的主配置在最后 include 了 conf.d/*.conf 和 sites-enabled/*;sites-enabled/default 是个指向 sites-available/default 的符号链接,这就是 Debian 系「加站点 = 写 available + 建软链 enabled」的由来。
    • include 的相对路径是按配置文件所在目录解析的,不是按 --prefix。本机实测把配置放在 /tmp/ngx3/uv.conf 并写 include uwsgi_params;,nginx 报的是:
      1
      [emerg] open() "/tmp/ngx3/uwsgi_params" failed (2: No such file or directory) in /tmp/ngx3/uv.conf:8
      它并没有去 --prefix=/usr/share/nginx 找。跨目录复用配置片段时,写绝对路径最省事。

main(全局设置)

  • 内容

    1
    2
    3
    4
    5
    6
    7
    user  www-data;                 # worker 以哪个用户/组运行
    worker_processes auto; # 实测 8 核 → 8 个 worker
    worker_cpu_affinity auto; # 把 worker 绑到不同核,减少迁移
    pid /run/nginx.pid; # master 的 pid 文件
    error_log /var/log/nginx/error.log notice; # 全局错误日志 + 级别
    worker_rlimit_nofile 65535; # 单进程可打开的最大文件描述符
    include /etc/nginx/modules-enabled/*.conf; # 动态模块,必须在最外层
    • 这几行不是凭空写的:user www-data;、worker_processes auto;、worker_cpu_affinity auto;、pid /run/nginx.pid;、error_log /var/log/nginx/error.log;、include /etc/nginx/modules-enabled/*.conf; 正是 Ubuntu 26.04 那份 /etc/nginx/nginx.conf 的开头六行(实测原文)。
  • user

    指定 worker 进程的运行用户与用户组,默认 nobody。master 仍以 root 运行(要绑 1024 以下端口、要写 pid 文件),只有 worker 降权。Ubuntu 包里是 www-data。

    • 权限模型值得记一句:worker 是低权限的,所以站点目录要让 www-data 能读;反过来,nginx.conf 里 user 指令本身只有 root 起的 master 能读到。
    • 原笔记「默认由 nobody 账号运行」在源码版成立;apt 装的会显式写 www-data。
  • worker_processes

    要开多少个 worker 子进程。auto 表示按 CPU 核数,实测 8 核 WSL 上 ps -C nginx 数出来正好 8 个 worker。

    • 原笔记「每个 Nginx 进程平均耗费 10M~12M 内存」是经验值,跟编译模块和数据量强相关,别当容量规划依据;真要估内存看 ps -o rss= -p <worker pid>。
    • 「一般指定 1 个进程就足够」这条要分场景:单核容器里 1 个是对的,多核机器上写 1 就是自废武功。
    • 实测 Windows 版也能开多 worker(worker_processes 2 → 1 master + 2 worker),网上那句「Windows 版只能一个 worker」已过时。
  • error_log

    全局错误日志的位置与级别,级别从低到高:

    1
    debug  info  notice  warn  error  crit  alert  emerg
    • 原笔记只列到 crit,缺 alert 和 emerg。
    • 级别是「记这个级别及更严重的」,不写级别时默认是 warn。实测只写 error_log /tmp/x.log; 时,启动全过程那些 notice 行一条都不会落盘(日志文件是空的);改成 ... error_log /tmp/x.log notice; 才出现下面这些行:
      1
      2
      3
      4
      5
      [notice] using the "epoll" event method
      [notice] start worker processes
      [notice] start worker process 22067
      [notice] signal process started
      [notice] getrlimit(RLIMIT_NOFILE): 10240:1048576
      写 error 就只剩真正出错的那几行。
    • 顺带一个细节:error_log stderr; 是编译期默认(nginx -V 里 --error-log-path=stderr),这也是为什么 nginx -t 的错误直接打在终端上。
    • debug 级别只有编译时带 --with-debug 才有输出,Ubuntu 包带了,源码版本文没带。
    • 每条日志都带 连接序号 与上下文,排查时非常关键,实测一条完整记录长这样:
      1
      2
      2026/11/04 02:00:18 [error] 23186#23186: *31 limiting requests, excess: 3.986 by zone "req2",
      client: 127.0.0.1, server: , request: "GET /req-file/page.txt HTTP/1.1", host: "127.0.0.1:8112"
      23186#23186 是 进程id#线程id,*31 是这条连接上的请求计数。
  • pid

    master 进程 pid 的存放位置。nginx -s 就是靠它找到该给谁发信号;systemd 则靠单元文件里的 PIDFile=。两者指向不一致时就会出现「nginx -s reload 说 kill() failed (2: No such process)」。

  • worker_rlimit_nofile

    单个 nginx 进程允许打开的最大文件描述符数。实测它是真的改得动系统上限:

    1
    worker_rlimit_nofile 4096;
    1
    grep -i 'open files' /proc/$(pgrep -P $(cat /tmp/ngxp/logs/rl.pid) | head -1)/limits
    1
    Max open files            4096                 4096                 files
    • 不写这行时,worker 继承的是系统默认:实测 Max open files 10240 1048576(软限 10240、硬限 1048576),error.log 里那句 getrlimit(RLIMIT_NOFILE): 10240:1048576 就是它。
    • 原笔记说「可以用命令 ulimit -n 65535 来设置」——那只影响启动 nginx 的那个 shell,写进 /etc/security/limits.conf 或 systemd 单元的 LimitNOFILE= 才持久。优先级上:worker_rlimit_nofile 是 nginx 自己调 setrlimit,比外部 ulimit 更可靠。
    • 这条与 worker_connections 的关系是:worker_connections 不能超过 worker 进程的文件描述符上限,否则会出现「配置写了 10240 却实际接不住」。
  • 原笔记那句「worker_rlimit_nofile 达到上限时报 502」

    这句要更正。实测撞上连接数上限时报的是 alert 级日志,且不一定给客户端 502:

    1
    [alert] 64 worker_connections are not enough while connecting to upstream
    • 也就是说:先撞的往往是 worker_connections(events 段)而不是 worker_rlimit_nofile;nofile 不够时报文会变成 open() ... failed (24: Too many open files)。两种都排查过才敢下结论。

events

  • 作用

    指定 nginx 的工作模型(用哪种 IO 多路复用)与每个 worker 的连接数上限。

  • 内容

    1
    2
    3
    4
    5
    events {
    use epoll; # Linux 上可省略,默认就是 epoll
    worker_connections 1024; # Ubuntu 包默认写的是 768
    multi_accept on; # 一次 accept 尽可能多的新连接
    }
  • use

    • 作用:指定事件模型。实测不用写——Linux 上 nginx 自动选 epoll,error.log 里那句 using the "epoll" event method 就是它自己选的;Windows 版则是 using the "select" event method。

    • 可选模型(按平台可用性):

      模型 说明
      select 标准模型,所有平台都有;Windows 版实测用的就是它
      poll 标准模型,比 select 略好
      kqueue BSD/FreeBSD/macOS 的高效模型
      epoll Linux 2.6+ 的高效模型,本机默认
      /dev/poll Solaris 专用
      rtsig Linux 实时信号模型,早已不推荐
      eventport Solaris 10 的事件端口
    • 原笔记这张表里的 rtsig、/dev/poll 留着当历史知识即可,写在自己的配置里反而可能报 unknown event method。生产上建议整行删掉 use,让 nginx 自己选。

  • worker_connections

    • 定义每个 worker 能同时处理的连接数,官方默认值是 512(不是很多人以为的 1024);源码版自带的 nginx.conf 里显式写了 1024,Ubuntu 包写的是 768,这三份数字刚好是三个不同的口径。
    • 官方对它的说明里有一句常被忽略:这个数字包含所有连接,包括与后端建立的连接,不只是客户端侧。所以反向代理场景下它天然「减半」,这才是原笔记「除以 4」这类经验公式的来源。
    • 理论最大并发连接数 ≈ worker_processes × worker_connections。
    • 实测更准的口径是:一个 worker 的并发上限同时受 worker_connections 和文件描述符上限约束,nginx 启动时会打
      1
      [notice] getrlimit(RLIMIT_NOFILE): 10240:1048576
      并且每个 TCP 连接至少占 1 个 fd,代理场景占 2 个。所以「worker_connections 设多大」的正确问法是「worker_rlimit_nofile 给了多大」。
    • 撞上限的真实表现(实测把 worker_connections 设成 64,然后让反向代理自己转自己):
      1
      2026/11/04 02:00:18 [alert] 23186#23186: *96 64 worker_connections are not enough while connecting to upstream
      客户端拿到 500。这条比「502」更常见,值得记住。
  • multi_accept

    官方默认 off:关闭时 worker 每轮事件循环只 accept 一个新连接,开启后一次把就绪的连接全收完(kqueue 模型下这个参数被忽略)。Ubuntu 配置里就是注释掉的 # multi_accept on;。

http

  • 作用

    最核心的模块,负责 HTTP 服务的所有属性;server 与 upstream 都是它的子块。

  • 内容(Ubuntu 26.04 实测原文,去掉了注释)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    http {
    include mime.types;
    default_type application/octet-stream;

    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;
    server_tokens build;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    access_log /var/log/nginx/access.log;

    gzip on;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
    }
  • include

    设定文件的 MIME 类型,映射表在 mime.types 里(Ubuntu 那份实测 101 行,如 text/plain txt;、application/json json;)。告诉 nginx 某扩展名该以什么类型响应。

  • default_type

    没匹配到 MIME 时的兜底类型,默认 application/octet-stream,也就是浏览器直接下载。原笔记举的例子(没配 asp 环境时访问 asp 文件变成下载)现在没什么参考价值,但机制描述是对的。

    • 实测一个高频坑:响应类型是按 URI 的扩展名决定的,不是按磁盘上的文件名。
      1
      location /big { alias /tmp/ngx2/data/big.txt; }
      1
      Content-Type: application/octet-stream
      因为 URI 是 /big,没有扩展名,于是落到 default_type;同一个文件走 /root-combo/hello.txt 时才是 text/plain。这条直接解释了「gzip_types text/plain 明明配了却看不到 Content-Encoding: gzip」。
  • log_format 与 access_log

    1
    2
    3
    4
    5
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;
    • 顺序有硬要求:log_format 必须写在引用它的 access_log 之前,否则启动直接失败:
      1
      [emerg] unknown log format "main" in /etc/nginx/nginx_test.conf:9
    • 真实日志行长这样(本机实测):
      1
      127.0.0.1 - - [04/Nov/2026:01:59:32 +0800] "GET /api/abc HTTP/1.1" 200 29 "-" "curl/8.18.0"
      字段依次是:客户端地址、远程用户($remote_user,没认证就是 -)、时间、请求行、状态码、发送给客户端的字节数、Referer、User-Agent、X-Forwarded-For。
    • 原笔记那段示例里的 "_" 应为 "-"(Nginx 用连字符表示空值),Mozilla/5.0 (Macintosh; Intel Mac OSX 10 .10_ 2) 是被 OCR 拆坏的旧 UA 串。
    • access_log off; 能关;access_log path format buffer=32k flush=5s; 能缓冲写;open_log_file_cache max=1000 inactive=20s; 能避免每条日志都 open() 一次(高并发下这个系统调用很贵)。
  • sendfile / tcp_nopush / tcp_nodelay

    1
    2
    3
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    • sendfile on 开启零拷贝式文件传输(内核直接把页缓存送到 socket,不经过用户态缓冲)。
    • tcp_nopush 只有在 sendfile on 时才有意义,作用是把响应头和数据攒成一个完整报文再发;tcp_nodelay 则在 keep-alive 连接上禁用 Nagle,保证小报文及时发出。
    • 原笔记的 sendfle、tcp_ nopush、tcp_ nodelay 都是 OCR 造成的错字(多个空格、少个字母),照抄会报 unknown directive。
    • Windows 版对 sendfile 不生效(它的事件模型是 select),这也是「Windows 版发静态文件慢」的原因之一。
  • keepalive_timeout

    客户端空闲连接的保持时间,默认 75s,源码版配置里显式写 65。设成 0 等于关闭长连接。配套的是 keepalive_requests(一条长连接上最多处理多少个请求,默认 1000)。

  • gzip

    1
    2
    3
    4
    gzip on;
    gzip_min_length 20;
    gzip_comp_level 5;
    gzip_types text/plain application/json;
    • 两个实测结论:
      1. gzip_min_length(默认 20 字节)以下的响应不会压缩——16 字节的 hello.txt 没有 Content-Encoding,301 字节的就有。
      2. 压缩只对 gzip_types 列出的类型生效,而类型又按 URI 扩展名判定(见上面 default_type 那条坑)。
    • text/html 是永远参与压缩的,不用写进 gzip_types;application/octet-stream 默认不在名单里,也别手贱加进去。
    • 建议再加 gzip_vary on;,否则 CDN/代理可能把压缩版发给不支持的客户端。
  • client_max_body_size

    请求体上限,默认 1m。实测设成 64 字节时:

    1
    2
    curl -s -o /dev/null -w '%{http_code}\n' --data-binary @b50.txt  http://127.0.0.1:8090/body/
    curl -s -o /dev/null -w '%{http_code}\n' --data-binary @b100.txt http://127.0.0.1:8090/body/
    1
    2
    200
    413
    • 上传文件报 413 就是这里,Django/Flask 自己的上限是第二道关卡,两边都要放行。
    • 0 表示不限制。
  • server_tokens

    控制 Server 响应头里暴露多少信息,三种取值本机实测:

    1
    2
    3
    on    -> Server: nginx/1.28.3
    build -> Server: nginx/1.28.3 (Ubuntu) # Ubuntu 默认就是它
    off -> Server: nginx
    • 生产建议 off。它写在 http、server、location 任一层都可以(实测放在 location 里也生效)。
  • types_hash_max_size / server_names_hash

    这两组是「哈希表容量」参数,Ubuntu 默认写 types_hash_max_size 2048;,server_names_hash_bucket_size 注释掉(自动)。原笔记没提,但网上大量「必须手动调 server_names_hash」的文章需要更正:实测把 server_names_hash_max_size 压到 16、bucket_size 压到 8,再放一个 45 字符的 server_name,nginx -t 和真实启动都不报错,nginx 会自己扩容。只有极老的版本才会报 could not build up the server name "hash"。

server

  • 作用

    定义一个虚拟主机:监听哪些地址端口、认领哪些域名、出错给什么页面、日志写到哪里。

  • 内容(Ubuntu 26.04 默认站点实测原文)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;

    root /var/www/html;
    index index.html index.htm index.nginx-debian.html;

    access_log /var/log/nginx/access.log;

    charset utf-8;

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    root /usr/share/nginx/html;
    }

    location / {
    try_files $uri $uri/ =404;
    }
    }
  • listen

    指定虚拟主机监听的地址与端口,可以写多次(IPv4 与 IPv6 各一条)。常见形式:

    1
    2
    3
    4
    5
    6
    7
    listen 80;                        # 所有 IPv4 地址的 80
    listen [::]:80; # IPv6
    listen 127.0.0.1:8080; # 只绑回环
    listen 80 default_server; # 没有任何 server_name 命中时归它
    listen 8443 ssl; # 这个端口直接讲 TLS
    listen 8080 reuseport; # 每个 worker 独立监听套接字(Linux)
    listen 80 proxy_protocol; # 前面还有一层 LB,需要解析 PROXY 头
    • default_server 是绑在 listen 上的,不是绑在 server_name 上的,这一点很多人记错。同一端口最多只能有一个 default_server,写两个会报 duplicate default server。
    • listen 8443 ssl 与 http2 的写法在新版本里是分开的(http2 on;),老写法 listen 443 ssl http2; 在 1.25 之后会报 the "http2" parameter of the "listen" directive is deprecated。本机 1.28.3 实测用的是 listen 8443 ssl; + http2 on;。
    • 端口被占用时报的是
      1
      [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
      这是「再起一个实例」时最常见的失败原因,先用 ss -ltnp 找到占用者。
  • server_name 与匹配优先级(实测)

    1
    2
    3
    4
    5
    6
    server { listen 8092 default_server; server_name _; return 200 "default-server\n"; }
    server { listen 8092; server_name foo.example.com; return 200 "exact-foo\n"; }
    server { listen 8092; server_name www.foo.example.com; return 200 "exact-www-foo\n"; }
    server { listen 8092; server_name .wild.example.com; return 200 "leading-dot-wildcard\n"; }
    server { listen 8092; server_name ~^regex(?<num>[0-9]+)\.example\.com$; return 200 "regex num=$num\n"; }
    server { listen 8092; server_name bar.example.com baz.example.com; return 200 "bar-or-baz\n"; }
    1
    2
    3
    for h in foo.example.com www.foo.example.com a.wild.example.com regex42.example.com bar.example.com other.test; do
    printf '%-26s -> %s\n' "$h" "$(curl -s -H "Host: $h" http://127.0.0.1:8092/)"
    done
    1
    2
    3
    4
    5
    6
    foo.example.com            -> exact-foo
    www.foo.example.com -> exact-www-foo
    a.wild.example.com -> leading-dot-wildcard
    regex42.example.com -> regex num=42
    bar.example.com -> bar-or-baz
    other.test -> default-server
    • 优先级是:精确名 > 以 */. 开头的通配名(长的优先)> 正则(按书写顺序)> default_server。
    • 前置点写法 .wild.example.com 同时匹配 wild.example.com 本身和所有子域,这是官方推荐的省哈希写法。
    • 正则名里可以用命名捕获 (?<num>...),捕获到的 $num 能在 location/rewrite 里用。
    • server_name 匹配的是请求的 Host 头,不是 IP。所以 curl http://127.0.0.1:8092/ 不带 -H Host 时命中的是 default_server;本地测试多虚拟主机时,curl --resolve foo.example.com:8092:127.0.0.1 比手写 -H 更接近浏览器行为。
  • charset

    给响应 Content-Type 追加 ; charset=utf-8。只对 charset_map 里配了映射的字符集生效(koi8-r、utf-8 等),所以写 charset utf-8; 是有效的。

  • access_log / error_log

    在 server 里可以覆盖全局的日志路径与格式。注意 access_log 里引用的格式名必须已经在 http 层用 log_format 定义过,否则就是那句 unknown log format "main"。

  • error_page

    1
    2
    3
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;
    location = /404.html { internal; }
    • 实测:请求一个不存在的 URI 时返回 404,响应体是 /tmp/ngx/html/404.html 的内容(15 字节的 not found page)。
    • internal 的作用是「只允许内部跳转访问」,直接请求 /404.html 会得到 404,防止用户绕过。
    • 还能写 error_page 404 =200 /empty.gif;(改状态码)与 error_page 404 = @fallback;(跳命名 location)。
    • 原笔记把 error_log 也列进 server 段并说「最后的 error 用于指定访问日志的输出格式」,那是笔误:error_log 的第二个参数是级别,不是格式名。
  • root 与 index(server 级)

    root 在 server 级表示整台虚拟主机的 web 根,index 定义默认首页文件名。要和本笔记后面 location 里的 root/alias 区分开——location 里再写 root 会覆盖 server 级的值。原笔记那句「注意要和 locate {} 下面定义的区分开来」里的 locate 是 location 的 OCR 错字。

location

  • 作用

    nginx 里用得最多也最重要的块:负载均衡、反向代理、虚拟域名都挂在它上面。它按 URI 定位处理规则,支持前缀匹配、正则匹配和条件判断,因此能对动静页面分别处理。

  • 六种匹配前缀

    写法 含义 实测
    = 精确匹配,命中即停,不再继续找 location = /exact 只吃 /exact,/exact/ 不算
    ^~ 匹配到该前缀后不再检查正则 ^~ /static/ 让 /static/pic.jpg 不被 ~* \.jpg$ 抢走
    ~ 区分大小写的正则,捕获组可用 ~ ^/re/(?<id>[0-9]+)$ → regex id=7
    ~* 不区分大小写的正则 ~* \.jpg$ 命中 /a.JPG
    无前缀 普通前缀匹配,多个命中时取最长的那个 /pref/longer 赢过 /pref/
    @name 命名 location,只能被 try_files/error_page 内部跳转 @fallback 输出 named fallback
  • 匹配优先级(实测结论)

    nginx 的查找顺序是:

    1. 先记录最长的普通前缀匹配(记住但先不执行);
    2. 如果这个最长前缀带 =,直接用它;
    3. 如果带 ^~,也直接用它,跳过所有正则;
    4. 否则按书写顺序逐个试正则,第一个命中的胜出;
    5. 正则都不命中,回到第 1 步记住的那个最长前缀。

    实测配置(8088):

    1
    2
    3
    4
    5
    6
    7
    8
    location = /exact        { return 200 "exact match\n"; }
    location ^~ /static/ { alias /tmp/ngx/static/; }
    location /pref/ { return 200 "prefix pref\n"; }
    location /pref/longer { return 200 "longer prefix\n"; }
    location ~ ^/re/(?<id>[0-9]+)$ { return 200 "regex id=$id\n"; }
    location ~* \.jpg$ { return 200 "case-insensitive jpg\n"; }
    location @fallback { return 200 "named fallback\n"; }
    location /try { try_files $uri $uri/ @fallback; }

    实测结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /exact                 -> exact match
    /exact/ -> not found page # = 不吃 /exact/,落到 server 根再 404
    /pref/longer -> longer prefix # 最长前缀赢
    /pref/xxx -> prefix pref
    /static/img/pic.txt -> jpg-real # ^~ 抢在正则前面,走 alias 拿到真实文件
    /a.JPG -> case-insensitive jpg
    /A.jpg -> case-insensitive jpg # ~* 不区分大小写
    /re/123 -> regex id=123
    /re/abc -> not found page # 正则没命中,也没有别的前缀规则
    /try/nothing-here -> named fallback # try_files 兜底到 @fallback
    /prefix-longest -> longest wins
    • 其中 /exact/ 和 /re/abc 这两条最能说明问题:没命中任何 location 规则时,请求落到 server 级 root 找文件,找不到就走 error_page 404,于是客户端看到的是那个 15 字节的 not found page,而不是 nginx 默认的 404 页。

    • 一句话记忆:= > ^~ > 正则(书写顺序)> 普通前缀(最长)。

    • 「正则按书写顺序」是很多人踩坑的地方:两条正则都能命中时,谁写在前面谁赢,跟长度无关。

  • 访问 nginx 本机目录下的文件

    1
    2
    3
    4
    location / {
    root html;
    index index.html index.htm;
    }
    1
    2
    3
    location /static/ {
    alias /var/static/;
    }
    • 这两段是原笔记里唯一没被 OCR 弄坏的配置,写法是对的;但要补上最关键的一句区别:
      • root 是「root + 完整 URI」;
      • alias 是「把 location 前缀整段替换掉」。
    • 实测证据(同一个文件 lim.txt 放在 /tmp/ngx/html/ 下):
      1
      location /limited2/ { root /tmp/ngx/html; }     # 请求 /limited2/lim.txt
      1
      open() "/tmp/ngx/html/limited2/lim.txt" failed (2: No such file or directory)
      它把 limited2/ 也拼进去了;而直接请求 /lim.txt(命中 server 级 root /tmp/ngx/html)是 200。
    • 用 alias 时,location 和 alias 结尾的 / 要成对:location /static/ { alias /var/static/; } 请求 /static/img/pic.txt → /var/static/img/pic.txt(实测拿到 jpg-real)。漏掉一边就会拼出 /var/staticimg/pic.txt 这种路径。
    • 老 nginx 在「alias + 正则 location + URI 被 rewrite 改写」的组合下有目录穿越漏洞(1.28.3 的 CHANGES 顶部那条安全修复就是 alias + WebDAV COPY/MOVE 的缓冲区溢出),所以能用 root 就别用 alias,非要 alias 就别在同一个 location 里做 rewrite。
  • 反向代理配置(Django runserver 场景)

    1
    2
    3
    4
    5
    6
    7
    location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $http_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_set_header X-real-ip $remote_addr; 和 proxy_pass http://www.suyin.tech:8000;。头名大小写不敏感,X-real-ip 能用但不规范,统一写 X-Real-IP;proxy_pass 指向公网域名再绕回本机是配置错误,本机跑就写 127.0.0.1:8000。
    • $host 与 $http_host 的实测差别,是这里最该记住的一条:
      1
      2
      3
      请求 Host: Foo.Example.com:1234
      $host -> foo.example.com (小写、去端口)
      $http_host -> Foo.Example.com:1234 (原样)
      Django 的 ALLOWED_HOSTS 要带端口、或者要做绝对 URL 拼接时,用 $http_host 才不会丢端口。
  • uwsgi 方式(Django 正式部署)

    1
    2
    3
    4
    location / {
    include /etc/nginx/uwsgi_params;
    uwsgi_pass suyin; # suyin 是 upstream 名字
    }
    • include 建议写绝对路径,理由见「配置」一节里那条实测:相对路径是按配置文件所在目录解析的。
    • 本机对这条只做到「配置合法」这一层:uwsgi_pass、scgi_pass、fastcgi_pass 三条指令在 1.28.3 上都能通过 nginx -t,且真实启动后能按各自协议去连后端,连不上时报
      1
      2
      [error] connect() failed (111: Connection refused) while connecting to upstream,
      upstream: "uwsgi://127.0.0.1:8000"
      客户端得到 502。本机没有装 uWSGI,因此「Django + uwsgi_pass 端到端跑通」这条没有实测,只验证到协议与配置层面;uwsgi://、fastcgi:// 这个前缀是判断走哪条协议链路的线索。
    • /etc/nginx/uwsgi_params 的内容(实测前几行)就是把 QUERY_STRING、REQUEST_METHOD、CONTENT_TYPE、CONTENT_LENGTH 这些 CGI 风格变量喂给 uwsgi:
      1
      2
      3
      4
      uwsgi_param  QUERY_STRING       $query_string;
      uwsgi_param REQUEST_METHOD $request_method;
      uwsgi_param CONTENT_TYPE $content_type;
      uwsgi_param CONTENT_LENGTH $content_length;
    • 同目录还有一份现成的 /etc/nginx/proxy_params(204 字节),内容正好是四条常用头,直接 include /etc/nginx/proxy_params; 比手抄省事:
      1
      2
      3
      4
      proxy_set_header Host $host$is_request_port$request_port;
      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;
      • 注意第一行:新版 Ubuntu 配置用 $is_request_port$request_port 来「客户端带了端口就保留端口」,比单纯写 $host 更稳。

静态资源与缓存

  • try_files

    1
    2
    3
    4
    5
    6
    7
    8
    9
    location / {
    try_files $uri $uri/ =404; # Ubuntu 默认站点就是这么写的
    }
    location /try {
    try_files $uri $uri/ @fallback; # 兜底到命名 location
    }
    location /app/ {
    try_files $uri $uri/ /index.html; # SPA 前端路由的标准写法
    }
    • 参数是候选列表,按顺序试:文件 → 目录 → 内部跳转目标;最后一个可以写成 =code、=404 或一个 URI。
    • 实测 /try/nothing-here → named fallback,说明前两个候选都不存在时确实跳到了 @fallback。
    • SPA(Vue/React 打包产物)必须写 try_files $uri $uri/ /index.html;,否则刷新子路由就是 404。
  • autoindex

    1
    location /dir/ { autoindex on; }

    实测返回的是 nginx 自带的目录页 HTML:

    1
    2
    3
    4
    5
    6
    7
    <html>
    <head><title>Index of /dir/</title></head>
    <body>
    <h1>Index of /dir/</h1><hr><pre><a href="../">../</a>
    <a href="a.txt">a.txt</a> 03-Nov-2026 16:00 3
    </pre><hr></body>
    </html>
    • 配套参数:autoindex_exact_size off;(显示 4.0K 而不是字节数)、autoindex_localtime on;(显示本地时区)。
    • 公网站点慎开,它等于把目录结构白送出去;真要当文件服务器用,配合 auth_basic 或 secure_link。
  • expires 与缓存头

    1
    location /exp/ { expires 1h; add_header X-Test hello; return 200 "cached\n"; }
    1
    2
    3
    Expires: Tue, 03 Nov 2026 17:00:56 GMT
    Cache-Control: max-age=3600
    X-Test: hello
    • expires epoch(或 off)实测得到的是「立刻过期」:
      1
      2
      Expires: Thu, 01 Jan 1970 00:00:01 GMT
      Cache-Control: no-cache
    • expires 会同时改 Expires 和 Cache-Control 两个头,别以为只写一个。
    • 静态资源的常见写法:
      1
      2
      3
      4
      5
      location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
      expires 30d;
      add_header Cache-Control "public, immutable";
      access_log off;
      }
  • add_header 的两个坑(实测)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server {
    listen 8090;
    add_header X-Srv server-level;
    add_header X-Srv2 also-server-level always;

    location /inherit/ {
    add_header X-Loc location-level; # 只要这里出现 add_header……
    return 200 "x\n";
    }
    }
    1
    2
    3
    4
    GET /big            -> X-Srv: server-level
    X-Srv2: also-server-level
    GET /inherit/ -> X-Loc: location-level # server 级的两条全部消失
    GET /nosuch (404) -> X-Srv2: also-server-level # 只有带 always 的还在
    • 坑一:add_header 是「整组覆盖」而不是追加。子级一旦写了任意一条,父级的所有 add_header 都不再生效。想保留就得在子级重写一遍。
    • 坑二:默认只对 2xx/3xx 响应生效。404、500 这类响应上你加的自定义头不会出现,必须写 always。
    • 同一层里写多条是允许的(X-Srv 和 X-Srv2 都生效)。
  • gzip 完整可用配置

    1
    2
    3
    4
    5
    6
    7
    gzip on;
    gzip_vary on;
    gzip_min_length 1024; # 太小不值得压
    gzip_comp_level 5; # 4~6 是常见甜区
    gzip_proxied any;
    gzip_types text/plain text/css application/json application/javascript
    text/xml application/xml application/xml+rss text/javascript image/svg+xml;
    • 实测两条边界:gzip_min_length 20 时 16 字节的 .txt 不压、301 字节的压;URI 没有扩展名时类型落到 application/octet-stream,不在 gzip_types 里就绝不压(哪怕只有 171 字节、哪怕 gzip on)。
    • gzip_static on(需要 --with-http_gzip_static_module,本机 Ubuntu 版编了)能直接发送预先压好的 xxx.gz,比每次现压省 CPU。
  • proxy_cache(实测能命中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    http {
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=zc:10m
    max_size=1g inactive=60m use_temp_path=off;
    }

    location /cached/ {
    proxy_pass http://127.0.0.1:8101;
    proxy_cache zc;
    proxy_cache_valid 200 1m;
    proxy_cache_key "$scheme$request_method$host$request_uri";
    add_header X-Cache $upstream_cache_status;
    add_header X-Backend $upstream_addr;
    }
    1
    2
    3
    第 1 次  X-Cache: MISS   X-Backend: 127.0.0.1:8101
    第 2 次 X-Cache: HIT
    第 3 次 X-Cache: HIT
    • 缓存文件确实落盘了,实测路径 /tmp/ngxp/cache/2/ea/8f3c5689b9cc74234650eea93b35dea2(levels=1:2 就是这两层目录名的由来)。
    • $upstream_cache_status 的取值:MISS/HIT/EXPIRED/STALE/BYPASS/UPDATING/REVALIDATED/STRONG_MISS,把它加进响应头或日志是排查缓存问题的第一步。
    • 没开 proxy_cache 的 location 上这个变量是空的——实测对照 location 的 X-Cache 就没有出现。
    • 缓存会吞掉「按用户区分」的响应,所以带 Cookie 的动态接口要么 proxy_cache_bypass $http_cookie;,要么干脆别缓。

反向代理指令详解

  • proxy_pass

    1
    2
    proxy_pass URL;
    # 上下文:location、if in location

    URL 可以是 http://host:port、http://upstream_name/,也可以是 unix:/path/to.sock;协议写 https:// 时 nginx 会把 HTTP 请求转成 HTTPS 发给上游(本机 Ubuntu 版编了 http_ssl_module,能用)。

    • 带不带 URI 路径,行为完全不同,这是反向代理第一大坑。实测:
      1
      2
      location /api/      { proxy_pass http://127.0.0.1:8101/; }      # 带 /
      location /apinouri/ { proxy_pass http://127.0.0.1:8101; } # 不带
      1
      2
      请求 /api/abc       -> 上游收到 /abc            (前缀被替换)
      请求 /apinouri/abc -> 上游收到 /apinouri/abc (原样透传)
    • 正则 location 里不能给 proxy_pass 带 URI,nginx -t 直接拒绝,原文:
      1
      2
      3
      [emerg] "proxy_pass" cannot have URI part in location given by regular expression,
      or inside named location, or inside "if" statement, or inside "limit_except" block
      in /etc/nginx/nginx_probe.conf:73
      正确写法是用 rewrite ... break 先把 URI 改好,再 proxy_pass 不带路径:
      1
      2
      3
      4
      location ~ ^/re-api/(.*)$ {
      rewrite ^/re-api/(.*)$ /$1 break;
      proxy_pass http://127.0.0.1:8101;
      }
      实测上游收到 /abc,与 /api/ 那条等价。
    • proxy_pass 里写变量时,nginx 不再在启动时解析域名,必须配 resolver;否则运行时报
      1
      [error] nosuchhost.invalid could not be resolved (3: Host not found)
      客户端得到 502。变量写法的好处是「上游地址能在运行时变」,做动态网关时才需要。
  • proxy_set_header

    1
    2
    3
    4
    proxy_set_header Host              $http_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;
    • 继承规则是「整组覆盖」:location 里只要写了任意一条 proxy_set_header,http/server 层的那些就全部不生效。实测某个 location 只写了一条 User-Agent "" 时,Host 回到默认的 $proxy_host、X-Real-IP 变成 None。
    • 把值设成空串等于删除这个头,实测:
      1
      proxy_set_header User-Agent "";
      1
      User-Agent: None        # 上游根本收不到 UA
      这是「抹掉某个客户端头」的标准做法,比 proxy_hide_header(那是管响应头的)更常用。
    • $proxy_add_x_forwarded_for 的语义实测:客户端已带 X-Forwarded-For: 203.0.113.7 时,上游收到 203.0.113.7, 127.0.0.1;客户端没带时就是 127.0.0.1。也就是说它是「追加本机 IP」,不是覆盖。
    • 默认就有的两条:Host: $proxy_host、Connection: close。
  • proxy_method

    1
    proxy_method GET;      # 上下文:http、server、location

    实测客户端用 POST 打 /m/,上游收到的是 GET:

    1
    curl -s -X POST -d 'a=1' http://127.0.0.1:8089/m/
    1
    backend 8101 /m/
    • 原笔记说「客户端发来的 GET 请求在转发时方法改为 POST」,方向讲反了:它是无条件把转发方法改成指定值,不管客户端原来是什么。生产上几乎不该用——把 POST 变 GET 会丢请求体语义,把 GET 变 POST 会破坏缓存与幂等。
  • proxy_http_version

    1
    2
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    • 默认 1.0,实测上游请求行就是 HTTP/1.0、Connection: close;改完是 HTTP/1.1。
    • 两条必须一起写:只改版本不清 Connection,nginx 仍会发 Connection: close,上游连接池(keepalive)和 WebSocket 都建不起来。
  • proxy_hide_header / proxy_pass_header

    1
    2
    3
    proxy_hide_header  X-Pad;
    proxy_hide_header X-Powered-By;
    proxy_pass_header Server; # 允许转发默认被隐藏的头
    • nginx 默认就不转发上游的 Date、Server、X-Pad、X-Accel-* 这几组头。实测上游(Python http.server)明明发了 X-Pad: browser,客户端侧 grep -c x-pad 是 0——不需要额外配置。
    • 于是 proxy_hide_header 的真实用途是「再隐藏一个默认会被转发的头」,比如后端吐的 X-Powered-By。
    • proxy_pass_header 反过来把默认隐藏的头放出来。实测 proxy_pass_header Server; 之后客户端看到的仍是 Server: nginx/1.28.3——Server 头是 nginx 自己重写的,不是简单转发,这条放了也没用。
    • 实测隐藏两个头的效果:
      1
      location /hide2/ { proxy_pass http://127.0.0.1:8101; proxy_hide_header X-Pad; proxy_hide_header Content-Type; }
      1
      2
      (对照默认)Server: nginx/1.28.3  +  Content-Type: text/plain
      /hide2/ Server: nginx/1.28.3 # Content-Type 也被抹掉了
      • 顺带一条真实告警:上游把 Server 头发了两遍时,nginx 会忽略后一个并记录
        1
        2
        [warn] upstream sent duplicate header line: "Server: backend-8101",
        previous value: "Server: BaseHTTP/0.6 Python/3.14.4", ignored while reading response header from upstream
  • proxy_pass_request_body / proxy_pass_request_headers

    1
    2
    3
    proxy_pass_request_body off;
    proxy_set_header Content-Length ""; # 必须一起清掉,否则上游会等 body
    proxy_pass_request_headers off; # 注意是复数 headers
    • 原笔记把指令名写成 proxy_pass_request_header(少一个 s),照抄会报 unknown directive。
    • 实测 proxy_pass_request_body off 时,nginx 不会顺手把方法改成 GET:上游 Python 服务没实现 do_POST,直接回了
      1
      Error code: 501  Message: Unsupported method ('POST').
      要改方法得再配一条 proxy_method GET;。这个组合的正当用途是「只探路径不传内容」,比如把 POST 转成一次无体的健康检查。
    • proxy_pass_request_headers off 之后上游收不到任何客户端请求头。实测同一请求(带 User-Agent: TestUA/9 和 Cookie: a=1)在开与关两种配置下上游看到的分别是:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      # proxy_pass_request_headers off
      GET /nohdr/hdrs HTTP/1.0
      Host: 127.0.0.1:8101 # 只剩 nginx 自己加的默认 Host(= $proxy_host)
      User-Agent: None
      Cookie: 不在列表里,同样没收到

      # 正常代理
      GET /hdrs/hdrs HTTP/1.0
      Host: 127.0.0.1 # 被 proxy_set_header Host $host 改过
      User-Agent: TestUA/9
      也就是说这个开关只影响「客户端头要不要透传」,nginx 自己那两条默认头(Host: $proxy_host、Connection: close)仍然照发。
  • proxy_redirect

    1
    2
    proxy_redirect [ default | off | redirect replacement ];
    # 默认 default;上下文:http、server、location

    上游返回 301/302 时,Location 头里往往是内网地址,浏览器拿到后根本访问不到。proxy_redirect 就是改写这个头。实测三种写法(上游 302 的 Location 是 http://127.0.0.1:8102/two/some/uri/):

    1
    2
    3
    location /redir/    { proxy_pass http://127.0.0.1:8102; proxy_redirect http://127.0.0.1:8102/two/ /one/; }
    location /redirdef/ { proxy_pass http://127.0.0.1:8102; proxy_redirect default; }
    location /rediroff/ { proxy_pass http://127.0.0.1:8102; proxy_redirect off; }
    1
    2
    3
    /redir/     -> Location: http://127.0.0.1:8089/one/some/uri/
    /redirdef/ -> Location: http://127.0.0.1:8089/two/some/uri/
    /rediroff/ -> Location: http://127.0.0.1:8102/two/some/uri/
    • 第一条说明 replacement 省略主机名时会用请求的 Host 补全(/one/... 变成 http://127.0.0.1:8089/one/...),原笔记「可以省略 replacement 参数的主机名部分,这时候用虚拟主机名填充」这句是对的。
    • 第二条说明 default 只替换 scheme://host:port,路径原样保留;原笔记里那段「proxy_redirect default 与显式写法等效」的示例(location / + proxy_pass http://upstream:port/two/)在 proxy_pass 不带 URI 时并不等效,实测就是上面这样。
    • off 完全不改写,客户端拿到内网地址——只有在你确认上游返回的是公网 URL 时才用。
  • proxy_next_upstream

    1
    2
    3
    proxy_next_upstream [ error | timeout | invalid_header | http_500 | http_502
    | http_503 | http_504 | http_404 | non_idempotent | off ];
    # 默认 error timeout;上下文:http、server、location
    • 语义:命中列出的条件时换下一台上游重试同一个请求。实测 upstream poolnext { server 8102; server 8101; } + error timeout http_500:
      1
      2
      3
      8102 对 /next/boom 返回 500
      客户端实际收到 -> backend 8101 /next/boom (200)
      error.log -> upstream server temporarily disabled while reading response header from upstream
    • 写成 off 时同一请求直接把 500 交给客户端:
      1
      /off/boom -> 500
    • 默认值 error timeout 不含 http_500,所以后端「活着但报错」时是不会切换的,必须显式加。
    • 非幂等请求默认不重试。官方 CHANGES 里 1.9.13 那条原文是:「non-idempotent requests (POST, PATCH) are no longer passed to the next server by default if a request has been sent to a backend; the non_idempotent parameter of the proxy_next_upstream directive explicitly allows retrying such requests.」——也就是说请求已经发出去之后,POST/PATCH 即使遇到 error/timeout 也不会换后端重试,必须显式写 non_idempotent 才会。这条能避免重复下单、重复扣款。
    • 原笔记那句「off 关闭功能,只要一出错就选择另外一台上游再次出发」把 off 的含义讲反了:off 是完全不换,一出错就把错误交给客户端。
  • 超时与缓冲

    1
    2
    3
    4
    5
    6
    7
    8
    proxy_connect_timeout 5s;     # 与上游建连的超时,默认 60s
    proxy_send_timeout 60s; # 向上游写请求的超时
    proxy_read_timeout 60s; # 等上游响应的超时(Django 慢接口最常调的就是它)
    proxy_buffering on; # 默认开
    proxy_buffer_size 8k; # 存响应头的缓冲区
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 64k;
    proxy_temp_path /var/lib/nginx/proxy; # 编译期默认值,见 nginx -V
    • proxy_read_timeout 到点报 504 Gateway Timeout,proxy_connect_timeout 到点报 502——这两个状态码能帮你区分「连不上」和「连上了但没回」。
    • 关闭缓冲(proxy_buffering off;)是 SSE/流式下载的必要条件,否则 nginx 会攒够一块才发给客户端。
    • 大文件下载或慢客户端场景,proxy_buffering off + proxy_request_buffering off 能显著降低内存与首字节延迟。

upstream

  • 作用

    负责负载均衡的模块:先把一组后端服务器声明成一个名字,再在 proxy_pass、uwsgi_pass、fastcgi_pass 里按名字调用。

  • 内容

    1
    2
    3
    4
    5
    6
    7
    8
    upstream suyin {
    ip_hash;
    server 10.11.0.1:8080;
    server 10.11.0.2:8080 down;
    server 10.11.0.3:8080 max_fails=3 fail_timeout=20s;
    server 10.11.0.4:8080 backup;
    keepalive 32;
    }
    • 名字(这里是 suyin)可以随便取,后面直接调用。
    • upstream 块里只能写 server 和这些算法/参数指令,写别的会报错;server 的地址可以是 IP:端口、域名:端口、unix:/path.sock,也可以只写 localhost:8080。
  • 调度算法一览(本机实测分布)

    指令 含义 实测
    (不写) 轮询,weight 调权 8 次 4:4;weight=3 对 1 时 12 次 10:2
    ip_hash 按客户端 IP 哈希 同 IP 6/6、10/10 固定一台
    hash key [consistent] 按自定义 key 哈希;key 里可用变量 hash $request_uri 让 /urihash/a、/b、/c 各自稳定;consistent 版把同 IP 映射到了另一台
    least_conn 优先当前活跃连接最少的 串行 6 次 3:3
    random [策略] 随机;可跟 least_time 等 random two 8 次全落一台(后端太快,选不出差别)
    least_time 按「最近响应时间 + 活跃连接」选 1.7.5+ 提供,本机可用
    sticky cookie name 需要商业版 Nginx Plus,开源版没有 写了会 unknown directive
    • 原笔记列的 fair 属于第三方模块,开源版确实没有;url_hash 现在就是 hash $request_uri,不需要额外装包。
    • 同一块里写两种算法时,nginx -t 能过,启动会打 [warn] load balancing method redefined,以最后写的为准(实测 ip_hash; least_conn; 之后 ip_hash 完全失效)。
  • server 的可用参数

    1
    2
    server address [weight=1] [max_fails=1] [fail_timeout=10s]
    [backup] [down] [route=name] [resize=128] [max_conns=0];
    参数 默认 说明
    weight 1 权值,平滑加权轮询用
    max_fails 1 在 fail_timeout 窗口内允许失败几次,超过就标记不可用
    fail_timeout 10s 失败计数窗口,同时也是「标记不可用」的暂停时长
    backup — 只有非 backup 全挂/全忙才用;与 ip_hash、hash 不兼容,实测 [emerg] balancing method does not support parameter "backup"
    down — 主动摘除,参与 backup 语义;与任何算法都能共存
    route / resize — 配合 hash ... consistent 与 sticky route 用
    max_conns 0(不限) 该后端并发连接上限;实测本机后端太快测不出效果
  • keepalive(上游连接复用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    upstream backend {
    server 127.0.0.1:8101;
    keepalive 32; # 每个 worker 与上游保持的空闲连接数
    }

    location / {
    proxy_pass http://backend;
    proxy_http_version 1.1; # 必须
    proxy_set_header Connection ""; # 必须
    }
    • keepalive 是「空闲连接池」而不是「最大连接数」,官方文档特别强调这点;它必须配合 proxy_http_version 1.1 + 清空 Connection 头,否则每条请求还是 HTTP/1.0 短连接,池子建不起来。
    • keepalive_requests(默认 1000)与 keepalive_timeout(默认 60s)也能写在 upstream 里,用于控制与上游的复用强度。

HTTPS 与 HTTP/2

  • 一份能跑的最小 HTTPS 配置(实测)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    server {
    listen 8443 ssl;
    http2 on; # 1.25 起的写法,不要再写 listen ... http2

    server_name localhost;
    ssl_certificate /etc/nginx/certs/server.crt;
    ssl_certificate_key /etc/nginx/certs/server.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_prefer_server_ciphers off;

    location / {
    return 200 "tls ok: protocol=$ssl_protocol cipher=$ssl_cipher\n";
    }
    }

    实测输出:

    1
    2
    tls ok: protocol=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384
    negotiated http_version = 2
    • 用 curl -sk -o /dev/null -w '%{http_version}\n' 验证 HTTP/2 协商成功;加 --tls-max 1.2 时也能连上(说明 ssl_protocols 两条都生效)。
    • 用 http 打 https 端口会得到 400 和一句明确的原因:
      1
      400 The plain HTTP request was sent to HTTPS port
    • 不带 -k 时 curl 报 rc=60(证书校验失败),自签证书是必然结果。
  • 自签证书怎么生成(实测)

    1
    2
    3
    4
    openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
    -keyout server.key -out server.crt \
    -subj "/C=CN/ST=ZJ/L=HZ/O=Loong/OU=Dev/CN=localhost" \
    -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
    • -addext subjectAltName 不能省。本机第一版没加,openssl x509 -ext subjectAltName 回的是 No extensions in certificate——现代浏览器和 curl 都只认 SAN,不认 CN,缺 SAN 的证书即使点了「继续访问」也可能被直接拒。
    • -nodes 表示私钥不加密(否则 nginx 启动要交互输口令,systemd 里会卡死)。
    • Debian 系还预置了一份「snakeoil」自签证书,/etc/nginx/snippets/snakeoil.conf 里两行 ssl_certificate/ssl_certificate_key 指向 /etc/ssl/certs/ssl-cert-snakeoil.pem 与 /etc/ssl/private/ssl-cert-snakeoil.key,测试环境直接 include snippets/snakeoil.conf; 就能起 https。
  • 正式证书与 HTTP→HTTPS 跳转

    1
    2
    3
    4
    5
    server {
    listen 80 default_server;
    server_name suyin-blog.cn;
    return 301 https://$host$request_uri;
    }
    • 实测 return 301 /target; 时 nginx 会把相对路径补成绝对 URL 再发出去:
      1
      2
      HTTP/1.1 301 Moved Permanently
      Location: http://127.0.0.1:8095/target
      所以 return 301 https://$host$request_uri; 这种写法是可靠的。
    • Let’s Encrypt 的证书用 certbot --nginx 自动签发与续期,它会往 sites-enabled 里写 listen 443 ssl 与 include /etc/letsencrypt/options-ssl-nginx.conf。
  • HTTP/3(QUIC)

    1
    2
    3
    4
    listen 443 quic reuseport;
    listen 443 ssl;
    http3 on;
    add_header Alt-Svc 'h3=":443"';
    • 本机 nginx -V 里确实带 --with-http_v3_module,配置能通过语法检查;但 WSL2 的 UDP 转发与 QUIC 握手不稳定,端到端没有实测成功,这里只作为写法记录。要验证得用 curl --http3 或浏览器 DevTools 看 h3。
  • TLS 相关的发行版默认值(实测原文)

    1
    2
    ssl_protocols TLSv1.2 TLSv1.3;   # Dropping SSLv3 (POODLE), TLS 1.0, 1.1
    ssl_prefer_server_ciphers off; # Don't force server cipher order.
    • Ubuntu 26.04 的默认配置里就这两行,注释写得很明白:SSLv3(POODLE)、TLS 1.0/1.1 已被剔除。原笔记完全没提 https,这块属于必须补上的内容。

WebSocket 转发与 map

  • 为什么 WebSocket 需要三件套

    WebSocket 握手是一个「带 Upgrade: websocket 的 HTTP 请求 + 之后切换成裸 TCP 流」。而 nginx 到上游默认是 HTTP/1.0 + Connection: close(实测过),协议切不过去。所以必须:

    1
    2
    3
    4
    5
    6
    7
    location /ws/ {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade; # 来自 map
    proxy_read_timeout 3600s; # 长连接别被超时掐断
    }
  • map 指令(实测)

    map 在 http 段里定义「根据一个变量的值给另一个变量赋值」:

    1
    2
    3
    4
    map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
    }

    实测两种输入:

    1
    2
    curl -s http://127.0.0.1:8090/map/
    curl -s -H 'Upgrade: websocket' http://127.0.0.1:8090/map/
    1
    2
    upgrade=[] conn=[close]
    upgrade=[websocket] conn=[upgrade]
    • 逻辑就是:客户端没发 Upgrade 头时 Connection 置为 close(普通请求照常工作),发了就置为 upgrade(放行握手)。这正是「一个 location 同时服务 HTTP 和 WebSocket」的标准做法。
    • map 的匹配规则支持精确串、~ 正则、~* 不区分大小写、hostnames 通配,default 是兜底。顺带一提,「用 ~/~* 前缀表示大小写敏感性」这个约定就是 1.0.4 那次改动定下来的(CHANGES 原文可查)。
    • 另一个常见用法是按 UA 分流:
      1
      2
      3
      4
      map $http_user_agent $is_mobile {
      default 0;
      ~*(Mobile|Android|iPhone|iPad) 1;
      }
      实测 if ($http_user_agent ~* Mobile) 的坑同样适用于这里:UA 串里必须真的出现 Mobile。本机用 Mozilla/5.0 (iPhone) 测试时命中的是桌面分支,补成 Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile/15E148 才走移动端。
  • 只给 WebSocket 用 Connection: upgrade 也行

    如果这个 location 只服务 WebSocket,可以直接写死:

    1
    2
    proxy_set_header Upgrade    websocket;
    proxy_set_header Connection upgrade;

    但一旦同一个 location 还要处理普通 HTTP 请求,就必须用上面的 map,否则普通请求会带上非法的 Connection: upgrade。

限流、访问控制与鉴权

  • 先记一条最容易踩的规则:这些指令对 return 型 location 不生效

    nginx 处理请求要依次经过若干阶段:

    1
    2
    3
    4
    post-read → server-name → rewrite → post-rewrite
    → preaccess(limit_req / limit_conn)
    → access(auth_basic / allow / deny)
    → try-files → content(静态文件 / proxy_pass / fastcgi …)

    return 是在 rewrite 阶段就把请求终结掉的,所以后面的 preaccess 与 access 阶段根本不会执行。实测同一个 location 只把内容来源换成静态文件,行为立刻不同:

    1
    2
    3
    4
    5
    6
    location /basic-return/ { auth_basic "r"; auth_basic_user_file /x/.htpasswd; return 200 "return-authed\n"; }
    location /basic-file/ { auth_basic "r"; auth_basic_user_file /x/.htpasswd; }
    location /deny-return/ { allow 10.0.0.0/8; deny all; return 200 "return-allowed\n"; }
    location /deny-file/ { allow 10.0.0.0/8; deny all; }
    location /limited/ { limit_req zone=perip burst=3 nodelay; return 200 "ok\n"; }
    location /limited2/ { limit_req zone=perip2 burst=3 nodelay; root /tmp/ngx/html; }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    auth_basic  + return   无凭证 -> 200 "return-authed"      # 完全没拦
    auth_basic + 静态文件 无凭证 -> 401,带 WWW-Authenticate: Basic realm="r"
    auth_basic + 静态文件 带凭证 -> 200 真实文件内容
    auth_basic + 静态文件 错密码 -> 401

    allow/deny + return 只允许 10/8,本机访问 -> 200 "return-allowed" # 没拦
    allow/deny + 静态文件 只允许 10/8,本机访问 -> 403

    limit_req + return 12 并发 -> 全 200
    limit_req + 静态文件 12 并发 -> 5 个 200 + 7 个 503
    • 换句话说:return 只能用来做「直接响应」,不能指望它配合鉴权、限流、访问控制。测试时也别用 return 的 location 去验证限流有没有生效,会得到「限流没用」的错误结论(我本人就在这上面翻过一次)。
    • proxy_pass 型 location 走的是 content 阶段,auth_basic、allow/deny、limit_req 都正常生效。
  • limit_req(按请求速率限流)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    http {
    limit_req_zone $binary_remote_addr zone=perip:10m rate=2r/s;
    }

    location / {
    limit_req zone=perip burst=5 nodelay;
    limit_req_status 429; # 默认是 503,改成 429 更语义化
    limit_req_log_level warn; # 默认是 error
    }
    • $binary_remote_addr 是「二进制的客户端 IP」,IPv4 固定 4 字节,比 $remote_addr 字符串省内存;10m 的共享内存大约能存 16 万个 IPv4 地址。
    • rate=2r/s 是「每秒放行 2 个」,burst=5 允许突发排队,nodelay 表示排队的那些不真的等待、直接放行,超出的返回 503。
    • 实测拒绝时的日志原文:
      1
      2
      [error] limiting requests, excess: 3.986 by zone "req2", client: 127.0.0.1,
      request: "GET /req-file/page.txt HTTP/1.1"
      excess 就是「超出速率多少」,调 burst 时看这个数字最直观。
  • limit_conn(按并发连接限流)

    1
    2
    3
    4
    5
    6
    7
    http {
    limit_conn_zone $binary_remote_addr zone=conc:10m;
    }
    location /download/ {
    limit_conn conc 2;
    limit_conn_status 429;
    }
    • 与 limit_req 的区别:一个管速率,一个管同时在进行的连接数,防下载/长连接场景要用后者。
    • 实测诚实记录:limit_conn conc 2 配 6 路并发请求(每个响应只有 17 字节、瞬间完成)全部返回 200——因为并发窗口太短,任何时刻活跃连接数没超过 2。要复现 503/429 得用慢后端或大文件。
  • auth_basic(HTTP 基础认证)

    1
    2
    3
    4
    # 生成密码文件(apr1 = MD5-crypt,OpenSSL 就能做,不必装 apache2-utils)
    printf 'probe:' > /etc/nginx/.htpasswd
    openssl passwd -apr1 '<你的密码>' >> /etc/nginx/.htpasswd
    chmod 644 /etc/nginx/.htpasswd
    1
    2
    3
    4
    5
    location /admin/ {
    auth_basic "secret-realm";
    auth_basic_user_file /etc/nginx/.htpasswd;
    proxy_pass http://127.0.0.1:8000;
    }

    实测:

    1
    2
    3
    无凭证   -> 401,响应头 WWW-Authenticate: Basic realm="secret-realm"
    带凭证 -> 200 正常内容
    错密码 -> 401
    • 权限给 644 就够(worker 以 www-data 读它),别给 600 否则报 open() failed (13: Permission denied)。
    • auth_basic off; 能在子 location 里关掉继承来的认证。
    • 基础认证是每次请求都要求凭证,配合 HTTPS 才不会明文传密码;生产上更常见的是用它保护 stub_status、/admin 这类内部入口。
  • allow / deny(IP 白名单)

    1
    2
    3
    4
    5
    6
    location /internal/ {
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
    proxy_pass http://127.0.0.1:8000;
    }
    • 规则自上而下匹配,命中即止,所以 allow 要写在 deny all 前面。
    • 实测本机访问 allow 10.0.0.0/8; deny all; 的静态 location → 403。
    • 注意它匹配的是 $remote_addr,如果前面还有一层 CDN/负载均衡,$remote_addr 是那一层的地址,此时要用 realip 模块(set_real_ip_from + real_ip_header X-Forwarded-For)把真实 IP 还原出来,否则白名单形同虚设。
  • stub_status(最简监控)

    1
    2
    3
    4
    5
    location /stub {
    stub_status;
    allow 127.0.0.1;
    deny all;
    }
    1
    2
    3
    4
    Active connections: 1
    server accepts handled requests
    15 15 15
    Reading: 0 Writing: 1 Waiting: 0
    • 三行数字依次是「累计接受连接数 / 成功握手数 / 已处理请求数」,第二行三个数不相等就说明有连接被丢弃或握手失败过。
    • Reading/Writing/Waiting 分别是「正在读请求头/体」「正在发响应」「保持空闲」。
    • 必须加访问控制,否则等于把流量指标公开。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    location /files/ {
    secure_link $arg_md5,$arg_expires;
    secure_link_md5 "secret$uri$arg_expires";
    if ($secure_link = "") { return 403; }
    if ($secure_link = "0") { return 410; }
    }

    location /api/ {
    limit_except POST PUT { deny all; } # 只放行写操作
    }
    • limit_except 里写的 location 内容不能再用 proxy_pass 带 URI,理由见前面那条 [emerg] "proxy_pass" cannot have URI part ... inside "limit_except" block。

rewrite 与 if

  • rewrite 的四种 flag(实测)

    1
    2
    3
    4
    5
    6
    7
    location /rw/       { rewrite ^/rw/([a-z]+)$ /pref/$1 last; }
    location /r301 { return 301 /target; }
    location /r302 { return 302 /target; }
    location /rw301 { rewrite ^/rw301$ /target permanent; }
    location /rw302 { rewrite ^/rw302$ /target; }
    location /loop { rewrite ^/loop$ /loop2; }
    location /loop2 { rewrite ^/loop2$ /loop; }
    1
    2
    3
    4
    5
    6
    7
    GET /rw/longer  -> longer prefix     # last:重新走一遍 location 匹配
    GET /rw/xxx -> prefix pref
    GET /r301 -> 301 Location: http://127.0.0.1:8095/target
    GET /r302 -> 302 Location: http://127.0.0.1:8095/target
    GET /rw301 -> 301 Location: http://127.0.0.1:8095/target # permanent
    GET /rw302 -> 200 (客户端看不到跳转) # 无 flag = 内部继续
    GET /loop -> 500
    flag 行为
    (不写) 改完 URI 后继续执行同一 location 里后面的 rewrite,最终按新 URI 找内容
    last 停止当前 rewrite 集合,用新 URI 重新匹配 location(最常用)
    break 停止当前 location 的 rewrite,直接用当前 URI 找内容,不再重新匹配 location
    redirect 返回 302,只在「替换的是完整 URI」时可用
    permanent 返回 301
    • 死循环有保护:rewrite 链超过 10 次就中断,日志原文
      1
      [error] rewrite or internal redirection cycle while processing "/loop2"
      客户端得到 500。看到 500 且日志里有 redirection cycle,先怀疑 last 与 try_files 互相指。
  • if 的真实用法与陷阱

    1
    2
    3
    4
    location /mob {
    if ($http_user_agent ~* Mobile) { return 200 "mobile\n"; }
    return 200 "desktop\n";
    }
    1
    2
    UA 含 Mobile -> mobile
    UA 是桌面串 -> desktop
    • nginx 作者本人说过「if is evil」,指的是 if 块里放 proxy_pass、root、try_files 这类内容生成指令时行为不可预期。安全用法只有两种:if 里只放 return 或 rewrite,或者只用来设置一个变量再交给 map/limit 之类判断。
    • 实测印证了这条:if 里写 return 完全正常(上面就是),但 if ($http_user_agent ~* Mobile) 里写 proxy_pass http://...; 就可能撞上「继承的 location 配置失效」的老问题。
    • 需要「按条件选后端」时,改用 map 生成变量再喂给 proxy_pass,比 if 干净得多。
    • 正则比较的写法是 ~(区分大小写)与 ~*(不区分),右边不要加引号:
      1
      2
      if ($http_user_agent ~* "Mobile") { ... }   # 老写法,能用但没必要
      if ($http_user_agent ~* (Mobile|iPhone)) { ... }
  • 变量与 set

    1
    2
    set $upstream_backend http://127.0.0.1:8101;
    proxy_pass $upstream_backend; # 变量形式:必须配 resolver,且失去启动期校验
    • 实测 proxy_pass 用变量指向一个解析不了的域名时报文是
      1
      [error] nosuchhost.invalid could not be resolved (3: Host not found)
      返回 502。变量代理很灵活,但代价是 nginx -t 阶段发现不了任何地址问题。

Django 项目部署(把前面几节串起来)

  • 场景一:runserver 直接反代(开发/演示)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    upstream django_app {
    server 127.0.0.1:8000;
    keepalive 16;
    }

    server {
    listen 80;
    server_name suyin-blog.cn;

    access_log /var/log/nginx/django.access.log main;
    error_log /var/log/nginx/django.error.log warn;

    location /static/ {
    alias /var/www/django/static/;
    expires 30d;
    access_log off;
    }

    location /media/ {
    alias /var/www/django/media/;
    }

    location / {
    proxy_pass http://django_app;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    include /etc/nginx/proxy_params;
    proxy_connect_timeout 5s;
    proxy_read_timeout 60s;
    client_max_body_size 20m;
    }
    }
    • 这套配置的每一块本文都有对应实测:upstream + keepalive 见「upstream」,alias 与 expires 见「静态资源」,proxy_params 与 client_max_body_size 见「http」与「反向代理指令」。
    • 本机用 8101 端口那个 Python HTTP 服务代替 runserver 跑过同样的结构,代理链路与头部转发均符合预期;Django 本身这条链路没有端到端实测(本机没起 Django 项目),ALLOWED_HOSTS、SECURE_PROXY_SSL_HEADER 这些 Django 侧配置请以 Django 官方文档为准。
    • runserver 是单线程开发服务器,只适合演示;正式部署走场景二。
  • 场景二:uWSGI(原文那两段的正确写法)

    uwsgi.ini:

    1
    2
    3
    4
    5
    6
    7
    [uwsgi]
    module = myproject.wsgi:application
    master = true
    processes = 4
    socket = 127.0.0.1:8001
    vacuum = true
    chmod-socket = 660

    nginx 侧:

    1
    2
    3
    4
    5
    6
    7
    8
    upstream suyin {
    server 127.0.0.1:8001;
    }

    location / {
    include /etc/nginx/uwsgi_params;
    uwsgi_pass suyin;
    }
    • 原笔记这里只有 include uwsgi_params; uwsgi_pass suyin; 两行,缺了「suyin 必须先在 http 里用 upstream 声明」这一层关系,读者照抄会报 no host in upstream。
    • socket 而不是 http:uwsgi 走的是自己的二进制协议,nginx 用 uwsgi_pass 讲它;如果 uwsgi 配成 http-socket,nginx 这边就该换成 proxy_pass。
    • 本机对这条只验证到「配置合法 + 协议链路会去连 8001」,实测连不上时报
      1
      2
      [error] connect() failed (111: Connection refused) while connecting to upstream,
      upstream: "uwsgi://127.0.0.1:8000"
      返回 502。未装 uWSGI,因此「Django 经 uwsgi_pass 正常出页」这条没有实测。
    • 现在更常见的替代是 Gunicorn + proxy_pass(gunicorn myproject.wsgi 监听 8000,nginx 侧就是场景一那套),配置更简单,也是本文其余部分实测过的链路。
  • 场景三:HTTPS + 静态资源 + 反代一条龙

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    server {
    listen 80;
    server_name suyin-blog.cn;
    return 301 https://$host$request_uri;
    }

    server {
    listen 443 ssl;
    http2 on;
    server_name suyin-blog.cn;

    ssl_certificate /etc/letsencrypt/live/suyin-blog.cn/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/suyin-blog.cn/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;

    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;

    location /static/ { alias /var/www/django/static/; expires 30d; }
    location /media/ { alias /var/www/django/media/; expires 7d; }

    location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Host $http_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;
    }
    }
    • 这里 X-Forwarded-Proto $scheme 是让后端知道「客户端其实走的是 https」,Django 侧要配合 SECURE_PROXY_SSL_HEADER;本机 TLS 终结部分实测过(protocol=TLSv1.3、http_version=2),Django 侧未实测。

常见报错与排查

  • 语法与启动类

    现象 / 日志原文 原因与处理
    [emerg] "server" directive is not allowed here in ...:7 层级写错,server 必须在 http 里。nginx -t 退出码 1
    [emerg] unknown log format "main" access_log ... main; 写在 log_format main 之前了
    [emerg] "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block 正则/命名 location/if/limit_except 里 proxy_pass 不能带路径,改用 rewrite ... break
    [emerg] balancing method does not support parameter "backup" ip_hash/hash 与 backup 互斥
    [warn] load balancing method redefined 同一 upstream 写了两种算法,后者生效
    [emerg] open() "/tmp/ngx3/uwsgi_params" failed (2: No such file or directory) include 的相对路径按配置文件所在目录解析,写绝对路径
    [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) 端口被占,ss -ltnp 找占用者,或 fuser -k 80/tcp
    [emerg] no host in upstream "" proxy_pass http://$http_host; 这类变量写法缺 resolver,或变量为空
    [emerg] the "stream" directive is not allowed here stream 是动态模块,apt install libnginx-mod-stream 后 nginx -T 里才看得到
  • 运行期状态码

    状态码 典型日志 真实原因
    400 The plain HTTP request was sent to HTTPS port 用 http 打了 https 端口(实测)
    403 — allow/deny 拒绝(实测本机访问只允许 10/8 的 location → 403)
    401 WWW-Authenticate: Basic realm="..." auth_basic 未带或带错凭证(实测)
    413 — 请求体超过 client_max_body_size(实测 64 字节上限时 100 字节请求体 → 413)
    404 open() "/tmp/ngx/html/limited2/lim.txt" failed (2: ...) root 语义理解错(root + 完整 URI),或 try_files 兜到 =404
    404 no resolver defined to resolve xxx proxy_pass 用了变量却没配 resolver
    500 rewrite or internal redirection cycle while processing "/loop2" rewrite/try_files 互相指,超过 10 次
    500 [alert] 64 worker_connections are not enough while connecting to upstream 连接数撞上限(实测由「自己代理自己」触发)
    502 connect() failed (111: Connection refused) while connecting to upstream 后端没起 / 地址端口写错(实测 uwsgi、fastcgi 两条链路都是这个)
    502 upstream server temporarily disabled while reading response header from upstream 命中 proxy_next_upstream 条件,该后端被临时摘除
    503 limiting requests, excess: 3.986 by zone "req2" limit_req 生效(实测)
    504 upstream timed out (110: Connection timed out) while reading response header proxy_read_timeout 太短,后端确实慢
  • 三条排查习惯

    1. 改完先 nginx -t,再看 error.log,最后才怀疑代码。-t 只报第一个致命错误,所以「改一处报一处」是正常现象,不要以为改坏了十处。
    2. nginx -T 是唯一能看清「到底加载了哪些文件、最终生效的配置是什么」的手段。sites-enabled 是符号链接、conf.d 与 sites-enabled 都可能定义同名 server,靠肉眼翻文件很容易看漏。
    3. 验证行为要用真实内容,不要用 return。这条是本文踩得最多次的坑:limit_req、limit_conn、auth_basic、allow/deny 在 return 型 location 上全部静默失效(实测),换成静态文件或 proxy_pass 才测得出真实效果。

参考