nginx 反向代理設定逐行解說
從 Ubuntu 24.04 建立 nginx 反向代理 server block,逐行說明 proxy_pass、4 個必要標頭、WebSocket、結尾斜線與檔案上傳設定。
nginx 反向代理設定的作用
nginx 反向代理會接收從 port 80 與 port 443 進入的請求,將每個請求轉交給已在本機連接埠上監聽的應用程式,然後把該應用程式的回應傳回瀏覽器。設定檔只有一個 server 區塊,而且內容很短。幾乎所有複雜性都集中在五、六行設定,這些設定會告訴應用程式真正的用戶端是誰,以及用戶端使用哪個通訊協定。
以下內容從 Ubuntu 24.04 的空白環境開始,使用發行版提供的 nginx 套件。起點是一個已在 127.0.0.1:3000 回應請求的應用程式。如果你尚未決定要使用哪個代理,請先閱讀 nginx 與 Caddy、Traefik 的比較。接下來會逐行說明 nginx 設定的寫法。
請在自己的伺服器上執行這些設定。重新載入前,先使用 sudo nginx -t 測試每次變更,並查看該指令輸出的內容。
Ubuntu 上 nginx 的設定檔位置
sudo apt update
sudo apt install -y nginx
ls -l /etc/nginx/sites-enabled/主要檔案是 /etc/nginx/nginx.conf。它在 http { } 區塊中設定全域選項,接著載入兩個目錄:/etc/nginx/conf.d/*.conf 和 /etc/nginx/sites-enabled/*。在 Ubuntu 和 Debian 上,請在 /etc/nginx/sites-available/ 中為每個網站建立一個檔案,再建立符號連結至 /etc/nginx/sites-enabled/ 以啟用網站。刪除符號連結即可停用網站,但保留檔案。
後文使用的兩個指令只能在 http context 中運作,不能放在 server 區塊內:map 和 upstream。請將它們放在 /etc/nginx/conf.d/ 下的獨立檔案中,因為該目錄會在 http 層級載入。
套件會啟用一個名為 default 的預設網站。它標記為 default_server,表示當請求的 Host 標頭在設定中找不到任何符合的 server_name 時,該網站會回應請求。在它保持啟用時,找不到你網站名稱的請求會落到這個網站,而不是你的應用程式。確認自有網站正常運作後,請移除該符號連結。
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx代理單一應用程式的最小 server block
server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}將其儲存為 /etc/nginx/sites-available/app.example.com,然後啟用並載入。
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -sI -H 'Host: app.example.com' http://127.0.0.1/listen 80; 綁定 IPv4,listen [::]:80; 綁定 IPv6。省略第二行時,DNS(domain name system)查詢若為伺服器傳回 AAAA 記錄的訪客,會收到 connection refused;而所有使用 IPv4 的人都會看到正常運作的網站。你收到的錯誤回報會是「我這邊可以運作」。
server_name 會比對瀏覽器傳送的 Host 標頭。可以使用空格分隔並列出多個名稱。若沒有任何 block 符合,nginx 會使用 default_server 的 block,因此必須移除套件提供的網站。
location / 會對請求路徑進行前綴比對,/ 則會比對所有路徑。proxy_pass 是 nginx 建立連線的位址。讓應用程式持續綁定在 127.0.0.1,使所有連線都只能經由 nginx 進入。若應用程式執行於容器中,請將其發布為 127.0.0.1:3000:3000,不要發布為 3000:3000,因為 Docker 會自行寫入規則,並直接繞過 ufw 發布連接埠;因此,無論防火牆設定為何,未受限制的發布連接埠都能從網際網路連線。
curl 行會讓伺服器本身傳送正確的 Host 標頭,因此你可以在 DNS 指向任何位置之前測試此 block。
nginx 未另行設定時傳送至 upstream 的內容
單獨使用 proxy_pass 會對應用程式隱藏 4 項資訊。
nginx 預設使用 HTTP/1.0 與後端通訊,並傳送 Connection: close。因此,每個請求都會建立新的 upstream 連線,也無法進行通訊協定升級。
Host 標頭會被改寫為 proxy_pass 中的值,也就是 127.0.0.1:3000。如果應用程式根據 Host 建立絕對連結,產生的連結就無法從伺服器外部開啟。
連線是由 nginx 轉送至應用程式,因此應用程式看到的用戶端位址是 127.0.0.1。應用程式中的每一行日誌與速率限制,記錄的都會是代理伺服器,而不是訪客。
應用程式無法判斷瀏覽器使用 HTTPS,因為它收到的是 loopback 位址上的純 HTTP 連線。
4 行設定即可修正上述所有問題。
要設定的 4 個標頭,以及每個標頭讓後端看見的內容
location / {
proxy_pass http://127.0.0.1:3000;
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;
}Host 傳遞訪客輸入的名稱。$host 是請求中的名稱,已移除連接埠並轉為小寫。設定後,應用程式才能建立正確的絕對 URL,例如登入後的重新導向 URL,或密碼重設電子郵件中的連結。省略此標頭時,這些 URL 會指向 127.0.0.1:3000,因此登入後瀏覽器會前往拒絕連線的位址。如果應用程式也需要連接埠,例如服務使用 8080,請使用 $http_host;這是用戶端傳送的原始標頭值。
X-Real-IP 傳遞單一值:$remote_addr,也就是 nginx 接受連線的位址。應用程式會讀取此值,寫入自己的存取日誌並執行自己的速率限制。
X-Forwarded-For 傳遞清單。$proxy_add_x_forwarded_for 會將 $remote_addr 附加到用戶端原本放在該標頭中的內容,因此值會以逗號分隔,而 nginx 加入的項目位於最後。這項細節會決定該標頭是否可信:用戶端可以傳送任意 X-Forwarded-For,因此讀取第一個項目的應用程式可能被告知任意位址。當 nginx 是邊緣伺服器時,請改寫成 $remote_addr,並捨棄用戶端傳送的值。當前方有 CDN 或其他代理伺服器時,請使用 realip 模組提供的 set_real_ip_from 和 real_ip_header,讓 $remote_addr 本身成為真正的用戶端位址。
X-Forwarded-Proto 傳遞 http 或 https。框架會讀取此值,決定是否將 Cookie 標記為 Secure,以及是否強制重新導向至 HTTPS。在 TLS 網站上省略此標頭時,設定為強制 HTTPS 的應用程式會看到 http,並回應重新導向至 HTTPS 位址。瀏覽器接著透過 nginx 傳送下一個請求,但應用程式仍看到 http,因此再次重新導向。最後瀏覽器放棄並顯示 ERR_TOO_MANY_REDIRECTS。
在每個 location 中重複這 4 行,會讓設定逐漸分歧。請將它們放在單一檔案中,再加以 include。
# /etc/nginx/snippets/proxy-headers.conf
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;location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
}這裡的繼承機制有一個陷阱。只有在 location 未定義任何自己的 proxy_set_header 指令時,該 location 才會從 server 區塊繼承這些指令。在 location 內加入一個 proxy_set_header 後,server 層級定義的所有標頭都會從該 location 移除。因此,請將所有標頭放在同一層級,或在每個執行 proxy 的 location 中 include 該片段。
為什麼我的 WebSocket 應用程式建立連線後又立即中斷?
因為預設設定會禁止 upgrade,而預設的讀取逾時會在 60 秒後關閉閒置通道。WebSocket 會先從包含 Upgrade: websocket 和 Connection: Upgrade 的 HTTP 請求開始。這些是 hop-by-hop 標頭,表示代理伺服器應該消耗這些標頭,而不是將它們轉送出去;此外,HTTP/1.0 完全沒有 upgrade 機制。這兩個標頭都必須手動加回。
map 應放在 http context 中,並置於獨立檔案內。
# /etc/nginx/conf.d/websocket.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}接著設定 location。
location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}設定 map 的目的是讓同一個 location 同時處理這兩種類型的流量。一般請求中的 $http_upgrade 為空,因此 $connection_upgrade 會變成 close。upgrade 請求中則會包含 websocket,因此傳送至 upstream 的標頭會是 Connection: upgrade。硬式寫入 proxy_set_header Connection "upgrade"; 會讓每個一般頁面請求也都傳送該標頭,而某些後端收到這類請求時會回應 400。
proxy_read_timeout 是造成「頁面載入後就停止更新」問題的原因。其預設值為 60 秒,計算的是兩次從後端讀取資料之間的間隔,而不是連線的存續時間。閒置 60 秒的 WebSocket 會由 nginx 關閉,瀏覽器主控台會顯示 socket 以代碼 1006 關閉。每分鐘內自行傳送 heartbeat 的應用程式不會遇到這個問題。未傳送 heartbeat 的應用程式則會在整整 1 分鐘後中斷。Live editor 和 dashboard 最先出現這個問題,透過 HTTPS 置於反向代理後的自架 n8n 實例就是常見範例。
proxy_pass 尾端的斜線為什麼會改變 URL?
規則只有一句話。如果 proxy_pass 以 URI(統一資源識別項)結尾,即使只是單獨的 /,nginx 也會移除請求路徑中符合 location 前綴的部分,並以該 URI 取代。如果 proxy_pass 只指定到主機和連接埠,則請求路徑會原封不動地傳遞。
location /app/ {
proxy_pass http://127.0.0.1:3000/;
}對 /app/status 的請求會以 /status 的形式抵達後端。
location /app/ {
proxy_pass http://127.0.0.1:3000;
}對 /app/status 的請求會以 /app/status 的形式抵達後端。
應使用哪種形式取決於應用程式。具有 base-path 或子資料夾設定的應用程式需要第二種形式,並在該設定中指定 /app。不支援前綴的應用程式則需要第一種形式。第一種形式的問題會立即出現:應用程式回傳的 HTML 仍包含 /static/main.css 這類絕對路徑,瀏覽器會向網站根目錄請求這些資源,但沒有任何 location 符合,導致頁面無法載入樣式。瀏覽器的網路分頁會顯示這些資產請求回傳 404。解決方式是設定應用程式本身的 base-path,或新增第二個 location /static/ 指向相同的後端。
regex location 無法在 proxy_pass 中帶有 URI。sudo nginx -t 會拒絕該設定,並指出原因:"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。
如果讓每個應用程式使用自己的名稱,例如 app.example.com,並從 location / 進行 proxy,這整類問題就會消失。只有在無法新增 DNS 記錄時,才值得處理子路徑。
如何讓同一個名稱後方對應多個後端?
使用 upstream 區塊。它屬於 http context,因此請在同一個檔案中的 server 區塊上方,或寫入 /etc/nginx/conf.d/。
upstream app_backend {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
keepalive 32;
}接著由 location 指定該名稱:proxy_pass http://app_backend;。
預設方法是 round robin。least_conn 會將每個請求傳送到目前連線數最少的後端,適合處理長度不一的請求。ip_hash 會將同一個 client address 固定對應到同一個後端。當應用程式將 session 保存在自身記憶體中時,需要使用 ip_hash,因為兩個這類後端採用 round robin 時,請求可能落到從未看過該使用者的 instance,導致使用者隨機登出。更好的做法是將 session 移至共享儲存空間。
max_fails=3 fail_timeout=30s 表示在 30 秒內連續失敗 3 次後,將該伺服器移出 30 秒。當區塊中的所有伺服器都處於此狀態時,client 會收到 502,error log 會顯示 no live upstreams while connecting to upstream。
keepalive 32 會讓每個 worker process 維持最多 32 個與後端的 idle connection,讓大多數請求不必重新進行 TCP handshake。這只能搭配 proxy_http_version 1.1 使用,且 upstream 不得有 Connection: close。如果同一個 location 也使用 WebSocket map,請將空值從 close 改為空字串,讓一般請求不帶 Connection header,並重複使用 pooled connection。
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}upstream 區塊中的名稱會在 nginx 啟動時解析。如果後端是容器,而容器重新啟動時會取得新的 address,nginx 會持續使用舊 address,直到重新載入設定。在 Docker network 內,可以使用內建 resolver 將查詢移至 request time。
resolver 127.0.0.11 valid=10s;
set $backend http://app:3000;
proxy_pass $backend;當容器頻繁建立與移除,導致你必須不斷修改 nginx 以跟上變化時,能讀取容器 labels 的 proxy 會是更好的工具。在多個 Docker Compose 應用程式前方使用 Traefik 會直接從容器本身建立路由。
為什麼上傳會失敗並顯示 413 Request Entity Too Large?
client_max_body_size 預設為 1 megabyte。較大的 request body 會在應用程式收到任何資料前遭 nginx 拒絕,錯誤日誌會記錄 client intended to send too large body。請在 server block 中提高此值,或在處理上傳的 location 中設定。
client_max_body_size 512m;將值設為 0 可完全停用這項檢查。應用程式本身也有大小限制,因此修改後若仍出現 413,表示錯誤來自 backend,下一步應檢查應用程式自身的上傳設定。
nginx 預設會先讀取完整的 request body,才建立 upstream 連線;較大的內容會先寫入磁碟上的暫存檔。這可避免 slow client 影響應用程式,因為 backend 會以本機完整速度接收上傳內容。對於非常大的上傳,可以改用串流方式。
proxy_request_buffering off;backend 會在內容到達時直接接收 request body,因此必須能夠處理這種方式。nginx 也會失去將請求重試至其他 upstream 的能力,因為 request body 已經讀取完畢。
client_body_timeout 預設為 60 seconds,限制的是 request body 兩次連續讀取之間的時間,而不是整個上傳的總時間。速度緩慢但持續的上傳可以通過這項限制。完全停滯的上傳則會被中止。
回應緩衝,以及會中斷即時輸出的設定
proxy_buffering預設為啟用,通常也是所需的設定。nginx 會以應用程式寫入回應的速度讀取資料,先暫存回應,再依慢速用戶端自身的速度傳送。應用程式工作程序可提早結束,不必在整個慢速下載期間持續忙碌。
這會中斷串流回應。Server-sent events 和即時日誌輸出在緩衝區填滿前,不會向讀取端顯示任何內容。只在該 location 中關閉緩衝。
proxy_buffering off;如果您能控制應用程式,更好的做法是只在串流回應中傳送 X-Accel-Buffering: no 標頭。nginx 會針對每個回應讀取該標頭,只對該回應停用緩衝,讓一般頁面繼續受益。
當 error log 顯示 upstream sent too big header while reading response header from upstream 時,表示回應標頭無法容納在單一緩衝區中。proxy_buffer_size預設為單一記憶體頁面,依平台而定為 4 或 8 kilobytes;過長的 cookie 或大型驗證標頭可能使其溢位。請同時提高這兩個值。
proxy_buffer_size 16k;
proxy_buffers 8 16k;TLS 應該放在此設定的哪一層?
放在 nginx,位於上述所有服務之前。TLS(傳輸層安全性)在代理層終止,nginx 到應用程式的連線則透過 loopback 位址使用純 HTTP,網路上的其他主機無法讀取這段流量。應用程式會從 X-Forwarded-Proto 得知訪客使用 HTTPS;這是 4 個標頭中的第 4 個。
不要手動填寫憑證路徑。將 DNS 記錄指向伺服器、開啟防火牆,並讓 Certbot 編輯同一個 server block:它會加入 listen 443 ssl 行與 ssl_certificate 路徑,並將 port 80 的要求重新導向。使用 Certbot 為 nginx 申請 Let’s Encrypt 憑證說明憑證申請流程與 renewal timer。
sudo ufw allow 'Nginx Full'
sudo ufw statusNginx Full 是 nginx 套件安裝的應用程式設定檔,會同時開啟 port 80 與 port 443。即使所有訪客都已重新導向至 HTTPS,port 80 仍必須保持開啟,才能完成 HTTP-01 renewal challenge。
先測試設定,再重新載入
sudo nginx -t
sudo systemctl reload nginxnginx -t 會解析所有包含的檔案,並回報測試成功,或顯示停止處的檔案與行號。重新載入前,請先閱讀該輸出。設定損毀時,重新載入不會套用變更:nginx 會繼續使用先前的設定提供服務,因此網站仍可運作,但您的變更會無聲地失效。systemctl restart 的行為則不同,而且結果更糟,因為重新啟動會先停止目前執行中的伺服器;設定錯誤會導致 nginx 完全未執行。預設使用重新載入,只有少數確實需要重新啟動的變更才使用 restart。
sudo tail -f /var/log/nginx/error.log
sudo ss -lntp | grep -E ':(80|443|3000)'ss 行會顯示每個埠由哪個程序持有,因此您可以確認應用程式確實在 proxy_pass 指定的位置監聽。
實際會遇到的故障
502 Bad Gateway,錯誤日誌中出現 connect() failed (111: Connection refused) while connecting to upstream。 proxy_pass 中的位址沒有任何程式正在監聽。應用程式可能已停止、繫結至其他連接埠,或繫結至主機無法連線的容器內部位址。
502,並出現 no live upstreams while connecting to upstream。 upstream 區塊中的每部伺服器,目前都已被 max_fails 標記為失敗。請修復後端服務。fail_timeout 到期後,nginx 會再次嘗試連線。
504 Gateway Time-out,並出現 upstream timed out (110: Connection timed out) while reading response header from upstream。 後端已接受連線,但在 proxy_read_timeout 秒內沒有傳送任何資料。對於確實執行緩慢的報表,提高逾時值是正確作法;但如果應用程式已卡住,這樣做並不能解決問題。
所有路徑都由應用程式回傳 404。 結尾斜線規則改寫了路徑。請比較應用程式日誌記錄的路徑與實際要求的路徑。
由其他網站回應。 server_name 不符合 Host 標頭,因此要求落入 default_server 區塊。
頁面載入後,介面約 1 分鐘後停止回應。 這是 WebSocket 情況:缺少 Upgrade 設定,或 proxy_read_timeout 仍為 60 秒。
FAQ
為什麼加入 proxy_pass 後,nginx 會回傳 502 Bad Gateway?
nginx 無法連線到 proxy_pass 中的位址。/var/log/nginx/error.log 中的錯誤日誌會指出原因:connect() failed (111: Connection refused) while connecting to upstream 表示該處沒有程序監聽,而 no live upstreams 表示 upstream 區塊中的所有伺服器都已標記為失敗。執行 sudo ss -lntp | grep 3000,查看哪個程序佔用該埠,以及程序繫結到哪個位址。若應用程式繫結到容器內部位址,或監聽的埠與設定中的埠不同,就會一直發生這個錯誤。
為什麼我的應用程式透過 nginx 運作約 1 分鐘後會中斷連線?
這是 WebSocket 連線,而 proxy_read_timeout 仍使用預設值 60 秒。此設定計算的是後端兩次讀取之間允許的間隔。閒置的 socket 會被 nginx 關閉,瀏覽器主控台則會回報關閉碼 1006。設定 proxy_http_version 1.1,透過 Upgrade 和 Connection,並在 $http_upgrade 上使用 map,將 proxy_read_timeout 提高為例如 3600s。如果沒有 Upgrade 標頭,升級根本不會發生,因此應用程式會改用 polling,或完全不顯示即時更新。
proxy_pass 結尾的斜線重要嗎?
重要,而且會改變後端收到的路徑。使用 location /app/ 和 proxy_pass http://127.0.0.1:3000/ 時,對 /app/status 的請求會以 /status 抵達後端,因為主機與埠後的任何 URI 都會取代符合的 location 前綴。移除最後的斜線後,同一個請求會以 /app/status 抵達。移除前綴通常會破壞應用程式自己的資源連結。這些連結仍使用絕對路徑,之後會在網站根目錄回傳 404。因此,若應用程式設有 base-path,較適合使用會保留路徑的寫法。
為什麼我的應用程式記錄的每個訪客 IP 位址都是 127.0.0.1?
因為應用程式實際收到的連線確實來自 loopback 位址上的 nginx。只有透過你設定的標頭,訪客位址才會傳給應用程式:單一值使用 proxy_set_header X-Real-IP $remote_addr;,附加的連線鏈使用 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;。接著,應用程式必須設定為信任這些標頭。請注意,客戶端可以自行傳送 X-Forwarded-For。因此,nginx 作為 edge server 時,應以 $remote_addr 覆寫該值,而不是附加內容。
nginx 與應用程式之間的連線需要 TLS 嗎?
不需要,前提是應用程式在同一台伺服器上執行,並繫結到 127.0.0.1,因為這些流量不會離開該機器。在 nginx 終止 TLS,讓 proxy_pass 透過 loopback 使用純 HTTP,並傳送 X-Forwarded-Proto $scheme,讓應用程式知道訪客使用的是 HTTPS。如果後端位於其他主機,且中間經過你不控管的網路,該跳轉就需要額外保護。你可以對後端使用 HTTPS,或在兩台機器之間建立 private tunnel。