Nginx 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器,以高并发处理能力和低资源消耗著称。安装 Nginx 只需要几条命令,几分钟就能跑起来;但一个网站真正的行为——它怎么分发请求、怎么代理到后端、怎么处理旧链接——全都写在配置文件里。本文只会简单带过安装,把大部分篇幅留给配置:配置文件是怎么组织的、location 匹配的优先顺序是什么、反向代理怎么写、静态资源怎么缓存、旧的 URL 怎么迁移。最后一节会给出本站生产环境实际在用的配置,作为一个可以直接对照阅读的完整例子。

1. 安装 Nginx

日常使用直接装包管理版本就够了:

sudo apt install nginx && sudo systemctl enable --now nginx   # Debian/Ubuntu
sudo dnf install nginx && sudo systemctl enable --now nginx   # CentOS/Fedora

配置文件在 /etc/nginx/nginx.conf,站点相关的配置通常放在 /etc/nginx/conf.d/*.conf(RPM 系)或 sites-available/ + sites-enabled/(Debian 系)。只有需要特定版本、或者需要包管理版本没编译进去的模块时,才值得从源码编译:装好编译依赖后 ./configure 加上要开启的模块、make && make install 即可,./configure 的参数只在编译时生效,以后想加模块就得重新编译整个二进制:

sudo apt install build-essential libpcre3-dev zlib1g-dev libssl-dev
wget http://nginx.org/download/nginx-<version>.tar.gz && tar -zxvf nginx-<version>.tar.gz
cd nginx-<version> && ./configure --prefix=/usr/local/nginx --with-http_ssl_module && make && sudo make install

2. 配置文件是怎么组织的

Nginx 的配置由一系列**指令(directive)块(context)**构成,块之间是嵌套关系,内层块会继承外层的配置,也可以覆盖它。核心的几层是:

main(全局)
 ├─ events { ... }   连接处理相关,比如 worker_connections
 ├─ http { ... }      HTTP 服务的总配置
 │   ├─ server { ... }        一个虚拟主机
 │   │   └─ location { ... }  一条 URL 匹配规则
 │   └─ server { ... }        另一个虚拟主机
 ├─ mail { ... }      邮件代理相关(用得较少)
 └─ stream { ... }    TCP/UDP 四层代理
  • main 层的指令直接写在文件最外层,例如 worker_processes(worker 进程数,一般设成 auto 让 Nginx 按 CPU 核心数自动决定)、pid(主进程 PID 文件位置)。
  • events 块只关心连接层面的参数,最常调的是 worker_connections,即单个 worker 能同时维持的连接数上限。
  • http 块是绝大多数配置真正发生的地方:日志格式、gzip、超时,以及下面挂的所有 server
  • 一个 http 块里可以有多个 server 块,靠 listenserver_name 区分不同的虚拟主机;一个 server 块里可以有多个 location 块,靠 URL 路径区分不同的处理方式。

实际项目里,http 块通常不会把所有内容写在一个文件里,而是用 include 把配置拆开:

http {
    include       /etc/nginx/mime.types;
    include       /etc/nginx/conf.d/*.conf;
    ...
}

这样每个站点、每个功能可以单独维护一个文件,改动互不干扰,也方便按需 include 一些独立的映射表(下面第 6 节会用到)。

3. server 块:一个虚拟主机长什么样

server {
    listen       80;
    server_name  example.com www.example.com;

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

    error_page  404              /404.html;
    location = /404.html {
        root   /var/www/html;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /var/www/html;
    }
}

listen 决定监听的端口(和可选的 IP),server_name 决定这个块响应哪些域名——同一个端口上可以挂很多个 server_name 不同的 server 块,Nginx 会按 Host 请求头匹配到对应的一个。root 指定这个虚拟主机的文件根目录,index 指定目录请求时依次尝试的默认文件。error_page 把特定状态码的响应重写到指定路径,常用来做统一的 404/50x 页面。

4. location 匹配规则与优先顺序

location 决定”这个 URL 该怎么处理”,Nginx 支持好几种匹配写法,理解它们的优先顺序比记住语法本身更重要:

  1. location = /path 精确匹配,只有 URL 完全等于 /path 才命中,优先顺序最高。
  2. location ^~ /prefix 前缀匹配,一旦匹配上就不再往下检查正则,优先顺序高于普通正则。
  3. location ~ /pattern / location ~* /pattern 正则匹配(~ 区分大小写,~* 不区分),按配置文件中出现的顺序依次尝试。
  4. location /prefix 普通前缀匹配,如果没有更优先的规则命中,取匹配最长的那个前缀。

也就是说,Nginx 不是”从上到下第一个匹配就用”,而是按这四类的优先顺序依次筛选,只有在精确匹配和 ^~ 前缀都没命中时才会去看正则。写多个 location 时最容易踩的坑就是以为它们是顺序生效的——实际上除了同类正则之间是顺序生效,其他类型之间是按优先顺序,不是按书写顺序。

location = /healthz {
    return 200 "ok\n";
}

location ~* \.(?:jpg|jpeg|png|svg|webp)$ {
    expires 30d;
    add_header Cache-Control "public";
}

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

try_fileslocation 里常用的一个指令:按顺序尝试后面列出的路径,第一个存在的就用它响应,全部不存在则用最后一个(这里是返回 404)。单页应用常见的写法是 try_files $uri $uri/ /index.html;,找不到静态文件时统一 fallback 到入口页面,交给前端路由处理。

5. 反向代理与 upstream

把 Nginx 放在后端服务前面做反向代理,核心指令是 proxy_pass

upstream app_backend {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://app_backend;
        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;
    }
}

upstream 块定义一组后端,默认按顺序轮询(round-robin),也可以加权重(server 127.0.0.1:3000 weight=3;)或换成 least_conn 等策略。转发请求时,后端默认只能看到来自 Nginx 的连接,看不到真实客户端信息,所以要靠 proxy_set_header 把原始的 Host、客户端 IP(X-Real-IP/X-Forwarded-For)和协议(X-Forwarded-Proto)透传过去,后端应用需要读这些头才能拿到真实的访问者信息,尤其是在做访问日志或按协议跳转时。

TLS 终止通常也放在反向代理这一层:证书配置在面向公网的 server 块(listen 443 ssl;ssl_certificatessl_certificate_key),Nginx 到后端之间可以继续用明文 HTTP,减少后端管理证书的负担。

6. 静态资源、压缩与缓存

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;

location ~* ^/(?:_astro|assets)/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    try_files $uri =404;
}

gzip 系列指令按类型压缩响应体,gzip_min_length 避免对本来就很小的响应做无意义的压缩。对于文件名里带 hash(构建产物常见做法)的静态资源,可以放心地给一个很长的 expires 加上 immutable:既然内容变了文件名一定会变,浏览器长期缓存旧文件名也不会拿到过期内容。没有 hash 的资源就不能这样设置,否则更新后用户可能长期看到旧版本。

7. 重定向与遗留 URL 迁移

网站改版、URL 结构调整之后,旧的描述性 slug 需要迁移到新 slug。做法是根据构建产物的 canonical URL 生成”旧路径 → 新路径”映射表,再由 Nginx 统一返回 301。本站的 canonical-paths.map 也处理尾部斜线、index.html、语言后缀和已淘汰的列表路径:

map $request_uri $request_path {
    ~^([^?]*) $1;
}

map $uri $canonical_path {
    default $request_path;
    include /etc/nginx/canonical-paths.map;
}

server {
    ...
    if ($request_path != $canonical_path) {
        return 301 https://aoi.ai$canonical_path$is_args$args;
    }
}

map 本身不做跳转,只是根据请求路径算出 canonical 路径;真正跳转的是后面那句 return 301。映射表由实际生成的 HTML 自动建立,因此新增页面不需要手写 location$is_args$args 保留原始查询字符串,绝对的 HTTPS apex 目标则同时收敛协议、主机名和路径,避免重定向链。

8. 一份完整的生产配置

把上面几节拼起来,就是本站生产环境实际在用的配置(略去了证书相关配置):

map $request_uri $request_path {
    ~^([^?]*) $1;
}

map $uri $canonical_path {
    default $request_path;
    include /etc/nginx/canonical-paths.map;
}

server {
    listen 8000;
    listen [::]:8000;
    server_name _;

    root /usr/share/nginx/html;
    index index.html;
    charset utf-8;
    absolute_redirect off;
    server_tokens off;

    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml application/javascript application/json application/rss+xml image/svg+xml;

    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    if ($request_path != $canonical_path) {
        return 301 https://aoi.ai$canonical_path$is_args$args;
    }

    location ~* ^/(?:_astro|article-assets)/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    location = /healthz {
        access_log off;
        default_type text/plain;
        return 200 "ok\n";
    }

    location / {
        try_files $uri $uri/ $uri/index.html =404;
    }
}

几个细节值得单独说一下:

  • server_tokens off; 让错误页面和响应头不再暴露 Nginx 的具体版本号,减少一点被针对性扫描的信息面。
  • add_header ... always; 里的 always 表示即使响应是错误页(4xx/5xx)也要带上这个头,默认情况下 add_header 在某些错误响应里会被跳过。
  • /healthz 单独关闭了 access_log,因为健康检查通常是几秒一次的高频请求,全部记录只会让日志被噪音淹没。
  • 最后的 try_files $uri $uri/ $uri/index.html =404; 是静态站点常见写法:先找精确文件,再找目录,再找目录下的 index.html,都找不到才返回 404——这是为了兼容 Astro 之类静态站点生成器输出的 about/index.html 这种目录结构。

9. 校验与重新加载

改完配置不要直接重启,先校验语法:

sudo nginx -t

确认没有报错之后,用 reload 而不是 restart

sudo systemctl reload nginx
# 或者
sudo nginx -s reload

reload 会让 Nginx 平滑地用新配置启动新的 worker 进程,等旧 worker 处理完当前连接后再退出,中间不会丢请求;restart 则会先杀掉进程再重新启动,短时间内会有服务中断。日常改配置、换证书、调整超时,都应该用 reload