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