NGINX 完整教學:從靜態網站到反向代理、HTTPS 與除錯

部分資料由 AI 生成。

NGINX 常放在網站最前面,負責接收瀏覽器送來的 HTTP/HTTPS 請求,再依網址、網域與路徑決定要回傳靜態檔案,或把請求轉交給 Python、Node.js、PHP 等後端服務。它也能處理 TLS 憑證、壓縮、快取、限流與負載平衡。

本文以 Ubuntu/Debian 與 NGINX Open Source 為主要示範環境。不同 Linux 發行版、套件來源與 NGINX 版本的預設路徑可能不同,修改前請先用文中的檢查指令確認實際環境。

本文最後更新於 2026 年 9 月 1 日。第一次設定網站時,可先讀「最安全的修改流程」「建立第一個靜態網站」與「設定反向代理」;遇到錯誤時,再從目錄跳到對應章節。

目錄

展開目錄

先看懂 NGINX 位於哪裡

瀏覽器通常不會直接連到應用程式。公開網站常見的請求路徑如下:

flowchart LR A[瀏覽器] -->|HTTPS 443| B[CDN/DNS 代理<br/>可選] B -->|HTTPS 或 HTTP| C[NGINX] A -. 未使用 CDN .-> C C -->|靜態檔案| D["/var/www/example"] C -->|反向代理| E[Python/Node.js<br/>127.0.0.1:8000] C -->|分流| F[其他後端服務]

NGINX 常見的工作包括:

工作 說明
Web 伺服器 直接傳送 HTML、CSS、JavaScript、圖片與下載檔案
反向代理 把公開請求轉交給只監聽本機連接埠的應用程式
TLS 終止 使用憑證處理 HTTPS,再把請求交給後端
負載平衡 把請求分配給多個後端服務
快取與壓縮 減少重複運算、頻寬與傳輸時間
存取控制 依 IP、密碼、請求速率或檔案路徑限制存取
日誌 記錄請求、狀態碼、耗時與錯誤原因

反向代理和正向代理的方向不同。反向代理代表伺服器,外部使用者不必知道後端程式的實際位址;正向代理則代表用戶端去存取外部網站。一般網站架設使用的是反向代理。

NGINX 的程序與設定概念

NGINX 啟動後,通常會有一個 master process 與數個 worker process。master process 讀取設定、管理 worker;worker 實際處理連線。重新載入設定時,NGINX 會嘗試啟動使用新設定的 worker,舊 worker 則在完成既有請求後結束。

flowchart TB M[master process<br/>讀取設定與管理程序] M --> W1[worker process] M --> W2[worker process] M --> W3[worker process] W1 --> R1[HTTP 請求] W2 --> R2[HTTP 請求] W3 --> R3[HTTP 請求]

設定檔由 directive(指令)與 context(區塊)組成。簡單指令以分號結尾,區塊則用大括號包住其他指令:

user www-data;
worker_processes auto;

events {
    worker_connections 768;
}

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

        location / {
            root /var/www/example;
        }
    }
}

主要層級如下:

Context 用途
main 設定 worker、PID、錯誤日誌等全域行為
events 連線處理方式與每個 worker 的連線數
http HTTP 共用設定、日誌格式、壓縮、快取與虛擬主機
server 一組監聽位址與網域規則,也稱 server block
location 依 URI 路徑選擇靜態檔案、代理或其他處理方式
upstream 定義一組後端伺服器,供反向代理或負載平衡使用

指令能放在哪一層,必須查該指令的官方文件。把 server 放進另一個 server,或把只允許出現在 http 的指令放到 locationnginx -t 會直接報錯。

安裝前先確認環境

不要先假設 NGINX 尚未安裝。先檢查執行檔、版本、編譯參數、服務狀態與實際設定檔位置:

command -v nginx
nginx -v
nginx -V
sudo systemctl status nginx --no-pager
sudo nginx -T

各指令用途:

指令 用途
nginx -v 顯示版本
nginx -V 顯示版本、編譯模組與編譯參數
nginx -t 檢查設定語法,並確認引用檔案能否開啟
nginx -T 檢查後輸出完整有效設定,包含 include 載入的檔案
systemctl status nginx 查看 systemd 服務狀態與近期訊息

nginx -T 可能印出網域、內部位址與憑證路徑。貼到公開討論區前先刪除敏感資料,且不要把私鑰內容貼出。

Ubuntu/Debian

從發行版套件庫安裝:

sudo apt update
sudo apt install nginx

安裝後檢查:

nginx -v
sudo nginx -t
sudo systemctl status nginx --no-pager

如果需要特定新版本或官方套件,請依 NGINX 官方安裝文件 設定套件來源,不要混用多個來源。切換套件來源前應備份設定並確認模組相容性。

RHEL/Rocky Linux/AlmaLinux/Fedora

套件名稱通常同樣是 nginx

sudo dnf install nginx
sudo systemctl enable --now nginx
sudo nginx -t

SELinux 啟用時,即使檔案權限看似正確,NGINX 仍可能無法讀檔或連到後端。不要用停用 SELinux 當作長期解法;應檢查安全脈絡與布林值。

Windows

NGINX 提供 Windows 版本,但官方文件把它定位為測試用途,功能與效能限制也和 Unix 版本不同。正式環境建議使用 Linux、WSL 內的測試環境,或容器。Windows 的控制指令可參考 NGINX 官方 Windows 文件

常見路徑與檔案配置

Ubuntu/Debian 套件常見的結構:

/etc/nginx/
├── nginx.conf
├── conf.d/
│   └── *.conf
├── sites-available/
│   └── example.com
├── sites-enabled/
│   └── example.com -> ../sites-available/example.com
├── snippets/
│   └── *.conf
└── mime.types

/var/log/nginx/
├── access.log
└── error.log

sites-availablesites-enabled 是 Debian 系套件慣例,不是 NGINX 核心功能。是否生效,仍要看 /etc/nginx/nginx.conf 裡是否有對應的 include

grep -n "include" /etc/nginx/nginx.conf
sudo nginx -T

常見載入方式:

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

不要在 sites-availablesites-enabled 各放一份會同時被載入的實體檔案,否則容易出現重複 server_name、重複監聽或修改錯檔。較清楚的做法是在 sites-available 保存實體檔案,再從 sites-enabled 建立符號連結。

最安全的修改流程

網站正在對外服務時,修改 NGINX 應遵守固定順序:

flowchart LR A[確認目前生效設定] --> B[備份目標檔案] B --> C[編輯設定] C --> D{sudo nginx -t} D -->|失敗| E[依行號修正<br/>不 reload] E --> C D -->|成功| F[systemctl reload nginx] F --> G[curl 與日誌驗證] G -->|異常| H[回復備份並再次測試]

1. 找出真正生效的設定

sudo nginx -T | less

也可搜尋網域與連接埠:

sudo nginx -T 2>&1 | grep -n "example.com"
sudo nginx -T 2>&1 | grep -n "listen 443"

2. 備份要修改的檔案

sudo cp -a /etc/nginx/sites-available/example.com \
  /etc/nginx/sites-available/example.com.bak-20260901-1530

備份名稱應含日期與時間,且不要覆蓋上一份備份。若設定由 Git、Ansible、Docker 映像檔或部署系統管理,應在來源端修改,避免伺服器上的手動變更被下一次部署蓋掉。

3. 編輯後先測試

sudo nginx -t

成功時會看到類似訊息:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

4. 重新載入,不必先重啟

sudo systemctl reload nginx

reload 會平順載入新設定。restart 會停止後再啟動,設定錯誤、連接埠衝突或啟動失敗時可能造成中斷。一般設定變更優先使用 reload

5. 從不同層級驗證

curl -I http://127.0.0.1/
curl -I -H 'Host: example.com' http://127.0.0.1/
curl -I https://example.com/
sudo tail -n 100 /var/log/nginx/error.log

若同一台主機放了多個網站,直接連 127.0.0.1 可能打到預設站台。加入 Host 標頭,才能測到指定的 server_name。HTTPS 站台若要略過外部 DNS,可用:

curl --resolve example.com:443:127.0.0.1 https://example.com/ -I

nginx -t 只證明語法與引用檔案能被讀取,不會證明 DNS、憑證鏈、後端應用程式、檔案權限、防火牆或公開網路都正常。

建立第一個靜態網站

假設網站檔案放在 /var/www/example.com/public

sudo mkdir -p /var/www/example.com/public

建立 index.html,並讓 NGINX 的執行帳號能沿路讀取目錄與檔案。不要為了省事把整個網站設成 777。靜態檔案常用目錄 755、檔案 644,實際擁有者則依部署流程決定。

建立 /etc/nginx/sites-available/example.com

server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;
    root /var/www/example.com/public;
    index index.html;

    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;

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

啟用站台:

sudo ln -s /etc/nginx/sites-available/example.com \
  /etc/nginx/sites-enabled/example.com
sudo nginx -t
sudo systemctl reload nginx

若符號連結已存在,先檢查它指向哪裡,不要直接用 ln -sf 蓋掉:

readlink -f /etc/nginx/sites-enabled/example.com

root 如何對應檔案

location /images/ {
    root /var/www/example.com/public;
}

請求 /images/logo.png 會對應:

/var/www/example.com/public/images/logo.png

root 會把完整 URI 接在指定目錄後面。這是最常見也最容易讀懂的寫法。

aliasroot 的差別

location /downloads/ {
    alias /srv/shared-files/;
}

請求 /downloads/manual.pdf 會對應:

/srv/shared-files/manual.pdf

alias 會以指定路徑取代符合的 location 前綴。尾端斜線不一致時很容易得到錯誤路徑。能用 root 清楚表達時優先用 root;必須把 URI 前綴映射到不同目錄時再用 alias

try_files 的用途

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

NGINX 會依序檢查實體檔案、目錄,最後回傳 404。它能避免所有不存在的路徑都誤落到首頁。

單頁應用程式(SPA)通常改成:

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

這讓 /profile 等前端路由回到 index.html。API 路徑應另外建立 location /api/,否則 API 的 404 可能被誤回成 HTML。

server_name 與多站台

NGINX 先依監聽位址、連接埠與 Host 選擇 server,再在該 server 裡比對 location

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/example.com/public;
}

default_server 屬於 listen,不是 server_name。它決定找不到合適 Host 時由哪個站台接手。return 444 是 NGINX 的非標準狀態,會直接關閉連線;若需要標準 HTTP 回應,可改用 return 404;

檢查 Host 比對:

curl -I -H 'Host: example.com' http://127.0.0.1/
curl -I -H 'Host: unknown.invalid' http://127.0.0.1/

location 的比對順序

常見形式:

location = /health { }
location ^~ /assets/ { }
location /api/ { }
location ~ \.php$ { }
location ~* \.(jpg|jpeg|png|gif)$ { }
location / { }
形式 意義
location = /path 完全相符,優先權最高
location ^~ /path/ 前綴相符,選中後不再檢查正規表示式
location /path/ 一般前綴;先找最長的相符前綴
location ~ pattern 區分大小寫的正規表示式
location ~* pattern 不分大小寫的正規表示式
location / 最後的通用前綴

簡化後的選擇流程:

flowchart TD A[收到 URI] --> B{有完全相符 = 嗎} B -->|有| C[使用完全相符 location] B -->|沒有| D[找最長前綴] D --> E{前綴有 ^~ 嗎} E -->|有| F[使用該前綴] E -->|沒有| G[依設定順序檢查正規表示式] G -->|第一個相符| H[使用該正規表示式] G -->|都不相符| I[使用先前最長前綴]

正規表示式 location 依出現順序比對,第一個相符者勝出。一般前綴則看長度,不看書寫順序。複雜規則可用小型測試檔與 curl 逐條驗證,不要只靠目測。

設定反向代理

假設 Python 或 Node.js 應用程式只監聽 127.0.0.1:8000

server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

建議讓後端只監聽 loopback 位址,避免同一服務同時從公開連接埠與 NGINX 暴露:

127.0.0.1:8000  建議,只有本機可連
0.0.0.0:8000    會監聽所有 IPv4 介面,需另行限制

確認後端是否真的在監聽:

sudo ss -ltnp
curl -I http://127.0.0.1:8000/

代理標頭

標頭 後端常見用途
Host 保留使用者請求的網域
X-Real-IP 傳遞 NGINX 看到的來源 IP
X-Forwarded-For 保存經過多層代理的來源 IP 鏈
X-Forwarded-Proto 告知後端外部請求是 HTTP 或 HTTPS

後端框架通常要設定可信任代理,才會採用這些標頭。若後端本身能被外部直接連線,攻擊者可自行偽造 X-Forwarded-For;不要未經限制就把它當成權限或稽核依據。

proxy_pass 尾端斜線

下列兩段行為不同:

location /api/ {
    proxy_pass http://127.0.0.1:8000;
}

請求 /api/users 會把原 URI 傳給後端,後端收到 /api/users

location /api/ {
    proxy_pass http://127.0.0.1:8000/;
}

proxy_pass 位址含 URI /,相符的 /api/ 前綴會被取代,後端收到 /users。修改斜線前先確認後端路由期待哪一種 URI。

逾時與串流

長時間工作、Server-Sent Events(SSE)或逐步產生回應的服務,可能需要調整:

location /events/ {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_buffering off;
    proxy_read_timeout 1h;
}

不要全站關閉 buffering 或把逾時無限制拉長。先確認該路徑的傳輸模式,再只調整需要的 location

WebSocket 反向代理

WebSocket 的 UpgradeConnection 是 hop-by-hop 標頭,反向代理時要明確傳遞。將 map 放在 http context:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

站台設定:

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

    location /socket/ {
        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;
        proxy_set_header Host $host;
        proxy_read_timeout 60s;
    }
}

若連線會長時間沒有資料,需讓應用程式定期傳送 ping,或依需求調整 proxy_read_timeout。測試時應使用真正的 WebSocket 用戶端,curl -I 不能完整證明升級後的雙向連線正常。

upstream 管理後端與負載平衡

upstream app_backend {
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
}

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

    location / {
        proxy_pass http://app_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

預設會以加權 round-robin 分配請求。常見方法:

upstream app_backend {
    least_conn;
    server 127.0.0.1:8001 weight=2;
    server 127.0.0.1:8002;
    server 127.0.0.1:8003 backup;
}
設定 用途
預設 round-robin 依權重輪流分配
least_conn 優先交給目前連線較少的後端
ip_hash 依來源 IP 盡量送到相同後端
weight=N 調整後端接收請求的比例
backup 主要後端不可用時才接收請求
max_failsfail_timeout 設定被動失敗判斷

開源版 NGINX 的基本 HTTP upstream 健康判斷多半是被動式:請求失敗後才暫時避開後端。不要把它誤認成完整的主動健康檢查系統。應用程式若保存登入狀態,優先把 session 放進共享儲存空間,而不是長期依賴黏著連線。

設定 HTTPS

公開登入、表單、API 與一般網站都應使用 HTTPS。流程分成 DNS、連接埠、防火牆、憑證與 NGINX 設定幾層;任何一層錯誤都可能讓簽發或連線失敗。

先完成 HTTP 與 DNS

申請憑證前確認:

dig +short example.com
curl -I http://example.com/
sudo ss -ltnp | grep -E ':80|:443'

網域的 A/AAAA 記錄要指向正確主機,且驗證方式所需的連接埠必須能從外部連入。若使用 CDN 代理,還要分清楚訪客到 CDN 與 CDN 到來源站的 TLS 設定。

使用 Certbot

Certbot 的安裝方式會隨作業系統與版本變動。先到 Certbot 官方指引 選擇作業系統與 NGINX,再依當前步驟安裝。

常見用法:

sudo certbot --nginx -d example.com -d www.example.com

只取得憑證、不讓 Certbot 改站台設定:

sudo certbot certonly --nginx -d example.com -d www.example.com

檢查自動更新:

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

renew --dry-run 會聯絡測試環境,適合驗證更新流程,但不能取代正式憑證到期日與服務載入狀態的監控。

手動引用憑證的基本結構

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example.com/public;
    index index.html;

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

私鑰不能放進公開 Git 儲存庫,也不要把權限改成所有人可讀。TLS 版本、cipher 與 session 設定應採用目前套件或 Certbot 產生的安全預設,再依支援的用戶端範圍調整,不要照抄多年以前的 cipher 清單。

HSTS 要延後啟用

add_header Strict-Transport-Security "max-age=31536000" always;

HSTS 會讓瀏覽器在指定期間強制使用 HTTPS。先確認所有子網域、憑證更新與 HTTP 轉址都穩定,再考慮 includeSubDomains 或提交到 preload 清單。錯誤啟用可能讓尚未支援 HTTPS 的子網域無法存取。

驗證憑證與轉址

curl -I http://example.com/
curl -I https://example.com/
openssl s_client -connect example.com:443 -servername example.com </dev/null

檢查憑證到期日:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

網域與網址轉址

www 統一轉到裸網域:

server {
    listen 80;
    listen [::]:80;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

固定路徑搬移:

location = /old-page {
    return 301 /new-page;
}

測試階段若不確定規則,可先用 302,確認網址、查詢字串與方法符合預期後再改成可快取的永久轉址。避免 rewrite 與多個 server 互相轉回,造成 ERR_TOO_MANY_REDIRECTS

靜態資源快取

帶內容雜湊的資源,例如 app.8f3a1c.js,可以設定長時間瀏覽器快取:

location ~* \.(css|js|png|jpg|jpeg|gif|svg|webp|ico|woff2)$ {
    add_header Cache-Control "public, max-age=2592000, immutable";
    try_files $uri =404;
}

若檔名不會隨內容改變,使用 immutable 可能讓使用者長時間拿到舊檔。HTML 通常不設同樣的長快取,因為它要負責指向新版資源。

代理快取

http context 定義快取區:

proxy_cache_path /var/cache/nginx/app
    levels=1:2
    keys_zone=app_cache:10m
    max_size=1g
    inactive=60m
    use_temp_path=off;

站台中啟用:

location /public-api/ {
    proxy_pass http://127.0.0.1:8000;
    proxy_cache app_cache;
    proxy_cache_valid 200 10m;
    add_header X-Cache-Status $upstream_cache_status always;
}

登入後頁面、購物車、個人資料、帶 Authorization 或 Set-Cookie 的回應不能未經設計就共用快取。先定義快取鍵、略過條件與清除方式,再啟用代理快取。

壓縮回應

基本 gzip 設定通常放在 http context:

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

JPEG、PNG、WebP、MP4 與 ZIP 等格式通常已壓縮,再用 gzip 效益有限且會增加 CPU 負擔。Brotli 並非所有 NGINX 套件都內建,先用 nginx -V 確認模組,不能只把網路上的 brotli on; 貼進設定。

上傳大小與請求逾時

限制請求本文大小:

server {
    client_max_body_size 20m;
}

超過限制通常回傳 413。若應用程式、容器入口、CDN 或負載平衡器也有限制,所有層級都要一起檢查。不要把限制直接改成極大值;先依實際檔案用途設定合理上限。

常見逾時:

client_header_timeout 10s;
client_body_timeout 30s;
send_timeout 60s;
keepalive_timeout 65s;

proxy_connect_timeout 是連到後端的時間,proxy_read_timeout 是兩次讀取動作間等待後端資料的時間。504 不代表只要把數字加大就能解決;也可能是後端卡住、資料庫過慢或網路不通。

限制請求速率與連線數

http context 定義共享區:

limit_req_zone $binary_remote_addr zone=login_rate:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=per_ip_conn:10m;

在特定路徑套用:

location = /login {
    limit_req zone=login_rate burst=5 nodelay;
    limit_conn per_ip_conn 10;
    proxy_pass http://127.0.0.1:8000;
}

限流鍵必須真的是使用者來源 IP。網站若在 CDN 或其他反向代理後面,NGINX 看到的 $remote_addr 可能只是上游代理。必須先安全設定 real IP,否則所有訪客可能共用同一個限額,或攻擊者能偽造來源。

先在測試環境觀察正常流量,再決定 rateburst。登入、API、圖片與整頁瀏覽需要的速率不同,不適合套同一組數字。

CDN 或上游代理後的真實 IP

只有在請求確定來自可信任代理時,才能採用它提供的來源 IP 標頭。概念如下:

set_real_ip_from 192.0.2.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

192.0.2.0/24 是文件示範網段,不能直接用於正式站台。若使用 Cloudflare,應從 Cloudflare 官方公布的 IP 範圍建立 set_real_ip_from,並安排更新。來源站也應限制只有可信任代理能連入;否則外部使用者可能繞過代理,自己送出偽造標頭。

套用後檢查 access log 的 $remote_addr、代理標頭與來源連線,確認沒有把任意外部標頭當成真實 IP。

基本存取控制

依 IP 允許或拒絕

location /admin/ {
    allow 192.0.2.10;
    allow 2001:db8::/32;
    deny all;

    proxy_pass http://127.0.0.1:8000;
}

規則依順序判斷。文件示範位址 192.0.2.0/242001:db8::/32 不可直接當成正式允許名單。

HTTP Basic Authentication

sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd admin
location /private/ {
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

不要把 .htpasswd 放在網站根目錄。Basic Authentication 的認證資料只是編碼,不是加密;必須搭配 HTTPS。

隱藏敏感檔案

location ~ /\. {
    deny all;
}

location ~* \.(env|ini|log|sql|bak)$ {
    deny all;
}

這類規則只是防線之一。真正的密鑰、備份與 .git 不應部署在可公開讀取的網站根目錄。加入規則後還要測試 ACME challenge、well-known 路徑與應用程式是否被誤擋。

安全標頭

可從低風險標頭開始:

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

Content-Security-Policy(CSP)必須依網站實際載入的 JavaScript、CSS、字型、圖片與外部服務設計。直接貼一份嚴格 CSP 可能讓登入、MathJax、分析工具、圖片或內嵌程式全部失效。先用 Content-Security-Policy-Report-Only 蒐集違規,再逐步收緊。

add_header 有繼承規則:子層若重新定義任一 add_header,可能不再繼承上一層同類指令。完成後要用 curl -I 檢查一般回應、錯誤頁、轉址與靜態檔案。

日誌與觀測

Ubuntu/Debian 常見日誌:

/var/log/nginx/access.log
/var/log/nginx/error.log

即時查看:

sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
sudo journalctl -u nginx -f

依時間查看 systemd 日誌:

sudo journalctl -u nginx --since "30 minutes ago" --no-pager

自訂代理耗時日誌

http context 定義格式:

log_format upstream_timing
    '$remote_addr - $host "$request" $status $body_bytes_sent '
    'request_time=$request_time '
    'upstream_addr=$upstream_addr '
    'upstream_status=$upstream_status '
    'upstream_connect_time=$upstream_connect_time '
    'upstream_response_time=$upstream_response_time';

站台中使用:

access_log /var/log/nginx/app.access.log upstream_timing;
error_log  /var/log/nginx/app.error.log warn;

$request_time 是 NGINX 處理整個請求的時間;$upstream_response_time 是等待後端回應的時間。兩者一起看,較容易分辨慢在用戶端傳輸、NGINX 還是後端。

日誌可能包含 IP、網址查詢字串、使用者代理與其他個人資料。設定保留期限、存取權限與輪替,且不要把密碼、token 或敏感表單資料放進 URL。

常用控制指令

指令 用途
sudo nginx -t 測試設定語法與引用檔案
sudo nginx -T 測試並輸出完整有效設定
sudo systemctl status nginx 查看服務狀態
sudo systemctl reload nginx 平順重新載入設定
sudo systemctl restart nginx 停止後重啟服務
sudo systemctl enable nginx 設定開機自動啟動
sudo systemctl disable nginx 取消開機自動啟動
sudo journalctl -u nginx 查看 systemd 日誌
sudo nginx -s reload 直接向 NGINX master 發送 reload 訊號
sudo nginx -s quit 平順停止,完成既有請求後離開

在 systemd 管理的主機上,日常操作優先用 systemctl,讓服務狀態與日誌維持一致。kill -9 只適合程序完全無法正常結束、且已理解後果的特殊情況。

常見 HTTP 狀態碼與排查方向

狀態碼 常見原因 先檢查
301/302 正常轉址或規則循環 Location 標頭、HTTP/HTTPS 與 CDN 規則
400 Host、請求格式、TLS 到錯誤連接埠 access log、請求標頭、監聽設定
403 權限、deny、目錄缺少索引、SELinux error log、路徑權限、location 規則
404 rootalias 對應錯誤或後端路由不存在 $uri 對應路徑、try_files、後端回應
413 請求本文超過限制 client_max_body_size 與上游限制
429 限流規則觸發 limit_req、真實 IP 與正常流量
499 用戶端在 NGINX 回應前斷線 後端耗時、用戶端逾時、網路品質
500 NGINX、FastCGI 或應用程式內部錯誤 error log 與應用程式 traceback
502 NGINX 無法從後端取得有效回應 後端是否監聽、socket 權限、協定
503 服務不可用、upstream 無可用成員 upstream 狀態、維護模式、容量
504 等待後端回應逾時 後端耗時、資料庫、網路、代理逾時

狀態碼顯示的是回應結果,不一定指出最初原因。例如 502 可能來自後端未啟動、位址填錯、Unix socket 權限不足、容器網路不同,或後端送出無效 HTTP。

403 Forbidden

先找 NGINX error log 的同一時間:

sudo tail -n 100 /var/log/nginx/error.log

檢查完整路徑的每一層權限:

namei -l /var/www/example.com/public/index.html
sudo -u www-data test -r /var/www/example.com/public/index.html \
  && echo readable

常見原因:

  • 目錄沒有可讀的索引檔,且 autoindex 關閉。
  • location 中有 deny all 或 IP 規則。
  • NGINX 執行帳號無法穿越上層目錄。
  • 檔案可讀,但 SELinux 或 AppArmor 阻擋。
  • CDN/WAF 回傳 403,請求尚未到來源站。

先看回應標頭與各層日誌,分清楚 403 是 CDN、NGINX 還是應用程式產生。

404 Not Found

nginx -T 確認真正載入的 serverlocation,再把 URI 按 rootalias 規則換算成檔案路徑:

sudo nginx -T 2>&1 | grep -n -A20 -B5 "server_name example.com"
namei -l /var/www/example.com/public/path/to/file

反向代理站台還要直接連後端:

curl -i http://127.0.0.1:8000/path/to/file

如果 NGINX 回 404 而後端正常,多半是 location、try_files 或 URI 改寫;如果後端直接回 404,應檢查應用程式路由。

502 Bad Gateway

依序檢查:

sudo ss -ltnp
curl -v http://127.0.0.1:8000/
sudo systemctl status my-app --no-pager
sudo journalctl -u my-app -n 100 --no-pager
sudo tail -n 100 /var/log/nginx/error.log

使用 Unix socket 時:

namei -l /run/my-app/app.sock
sudo -u www-data test -r /run/my-app/app.sock

socket 通常還需要 NGINX 執行帳號具備連線所需權限。不要用 chmod 777 掩蓋擁有者、群組或服務啟動流程的錯誤。

504 Gateway Timeout

先測後端實際耗時:

time curl -v http://127.0.0.1:8000/slow-path

再對照 access log 的 $request_time$upstream_response_time。若後端本身已超時,應查資料庫查詢、外部 API、鎖定、CPU、記憶體與工作佇列。只有確認該工作本來就合理地需要較長時間,才調高特定路徑的 proxy_read_timeout

轉址循環

常見情況是 CDN 對來源站使用 HTTP,但來源站依自身看到的 $scheme 強制轉回 HTTPS,或後端不信任 X-Forwarded-Proto,一直產生同一個轉址。

curl -IL --max-redirs 10 http://example.com/
curl -IL --max-redirs 10 https://example.com/

觀察每一步 Location。同時檢查 CDN TLS 模式、NGINX 的 HTTP 轉址與後端框架的 HTTPS 強制設定,只保留清楚且一致的責任分工。

設定測試成功,但網站仍失敗

flowchart TD A[nginx -t 成功] --> B{服務有監聽 80/443 嗎} B -->|沒有| C[查 systemctl、ss、連接埠衝突] B -->|有| D{本機帶 Host 測試正常嗎} D -->|沒有| E[查 server/location、權限與 error log] D -->|有| F{直接測後端正常嗎} F -->|沒有| G[查應用程式與 socket/連接埠] F -->|有| H{公開網址正常嗎} H -->|沒有| I[查 DNS、CDN、防火牆、TLS 與路由] H -->|有| J[完成分層驗證]

每一層都保留測試證據。不要看到公開網址錯誤就立刻修改 NGINX,也不要看到 nginx -t 成功就假定外部網站一定正常。

使用 Docker 時的差異

最小設定:

services:
  nginx:
    image: nginx:stable
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./public:/usr/share/nginx/html:ro

容器中的 127.0.0.1 指向容器自己,不是主機或另一個容器。若後端服務名稱是 app

location / {
    proxy_pass http://app:8000;
}

測試容器內設定:

docker compose exec nginx nginx -t
docker compose exec nginx nginx -T

重新載入:

docker compose exec nginx nginx -s reload

若設定檔是映像檔的一部分,正式變更應重新建置與部署映像檔,避免容器重建後遺失手動修改。固定映像標籤或 digest,並在升級前閱讀版本變更。

PHP-FPM 基本概念

PHP 通常透過 FastCGI 交給 PHP-FPM,不使用 proxy_pass

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

socket 名稱會隨 PHP 版本與套件改變,先確認:

systemctl list-units 'php*-fpm.service'
find /run/php -maxdepth 1 -type s -ls

不要把使用者可上傳的目錄任意交給 PHP 執行。SCRIPT_FILENAMEtry_files 與 location 比對若設定錯誤,可能造成檔案不存在、原始碼外洩或執行不該執行的檔案。優先從發行版提供的安全範本開始,再依專案調整。

設定維護方式

大型設定可拆成數個用途明確的 snippet:

/etc/nginx/snippets/
├── proxy-headers.conf
├── ssl-example.com.conf
└── security-headers.conf

引用:

location / {
    include /etc/nginx/snippets/proxy-headers.conf;
    proxy_pass http://127.0.0.1:8000;
}

拆檔不是越多越好。同一指令若在 httpserverlocation 與 snippet 重複出現,繼承關係會變得難以追蹤。用 nginx -T 查看最後載入結果,並把設定納入版本控制;私鑰、密碼檔與含秘密的設定另行管理。

設定差異與自動測試

修改前後可保存完整設定:

sudo nginx -T > /tmp/nginx-before.txt 2>&1
# 編輯並測試設定
sudo nginx -T > /tmp/nginx-after.txt 2>&1
diff -u /tmp/nginx-before.txt /tmp/nginx-after.txt

這些檔案可能含內部資訊,使用完應妥善清除。持續整合可在容器內執行 nginx -t,再以測試請求檢查轉址、標頭、靜態檔案與代理路由。

上線前檢查表

設定與服務

  • 已確認修改的是實際載入的檔案。
  • 已保留可辨識時間的備份或 Git 提交。
  • sudo nginx -t 成功。
  • 使用 reload 後,systemctl status nginx 仍為 active。
  • ss -ltnp 顯示預期的 IPv4/IPv6 監聽位址。

網站與後端

  • 伺服器本機帶正確 Host 測試成功。
  • 公開 HTTP 會轉到正確 HTTPS 網址,沒有循環。
  • 首頁、靜態檔案、API、404 與上傳都已測試。
  • 後端只暴露在預期的介面與連接埠。
  • WebSocket、SSE 或大型下載已用真正用戶端測試。

TLS 與安全

  • 憑證網域、完整鏈與到期日正確。
  • 自動更新已用 dry run 驗證,且有到期監控。
  • 私鑰與密碼檔不在網站根目錄或公開儲存庫。
  • CDN 後方的真實 IP 只信任已知代理來源。
  • 限流、上傳大小與安全標頭沒有擋到正常功能。
  • CSP 已用實際頁面及瀏覽器開發者工具測試。

觀測與回復

  • access log、error log 與 systemd 日誌都能讀取。
  • 日誌有輪替與合理保留期限。
  • 已知道發生錯誤時要回復哪個檔案或版本。
  • 回復後仍會再次執行 nginx -t、reload 與 HTTP 驗證。

一份可直接改寫的反向代理範本

下列範本適合單一後端應用程式。使用前請替換網域、連接埠、憑證路徑與需求相關設定:

upstream example_app {
    server 127.0.0.1:8000;
    keepalive 16;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://example.com$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;

    client_max_body_size 20m;

    location /static/ {
        alias /srv/example-app/static/;
        expires 7d;
    }

    location / {
        proxy_pass http://example_app;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

這份範本不包含 WebSocket、CDN real IP、代理快取、CSP 或特定框架設定。只加入網站真正需要、而且已測試的功能。

延伸閱讀

正式修改前,先保留現有設定,確認 nginx -T 的實際載入結果。每次變更都先通過 nginx -t,再 reload,最後從來源站與公開網址分層驗證。