Nginx、Caddy、Traefik 反向代理怎麼選?
比較 Nginx、Caddy 與 Traefik 在 TLS 憑證、每個應用程式的設定成本、WebSocket,以及 Docker 路由上的差異,協助你為單一 VPS 與公開 IP 選擇代理。
Nginx 與 Caddy、Traefik 的簡短結論
Nginx、Caddy 和 Traefik 都能執行反向代理的相同工作:監聽 443 埠、讀取每個請求中的主機名稱,然後將請求轉送到 VPS 上正確的服務。三者都能讓四個自架應用程式共用一個公開 IP 位址,而且速度都足夠快,實際上較慢的會是應用程式本身。差異在於各自如何取得 TLS(transport layer security)憑證,以及每增加一個應用程式需要多少設定。另一項差異會在之後出現,也就是你需要處理一般教學未涵蓋的情況時。
如果你希望由系統代為處理 HTTPS,而且服務都是一般 Web 應用程式,請選擇 Caddy。如果所有服務都執行在 Docker Compose 中,並且每隔幾週就會新增服務,請選擇 Traefik。如果你已經在使用 Nginx,或需要回應快取、用戶端憑證、原始 TCP 轉送,或已有大量設定且不想重新改寫,請選擇 Nginx。
每個服務如何取得 TLS 憑證?
這個面向通常會決定多數人的選擇,因此先從這裡開始。三者最後都會取得同一個憑證授權單位簽發的憑證,但取得憑證的操作方式不同。
Caddy 會因為你指定了主機名稱而申請憑證。 將 app.example.com 寫成站台位址後,Caddy 會透過 ACME(automatic certificate management environment)向 Let's Encrypt 申請憑證;若失敗,則改用 ZeroSSL。Caddy 也會在 port 80 提供 HTTP 到 HTTPS 的重新導向,並自行續期。不需要額外工具,也不需要設定 timer 進行檢查。憑證會儲存在 caddy 使用者的資料目錄中;套件安裝時位於 /var/lib/caddy/.local/share/caddy。請將該路徑加入備份,或接受重建後重新簽發憑證。對於不公開的主機名稱,tls internal 會改用 Caddy 自己的本機憑證授權單位簽署。這與在 Ubuntu 上建立自簽憑證的結果相同,但續期由 Caddy 自動處理。
Nginx 沒有 ACME client。 Certbot 會取得憑證,其 --nginx plugin 會改寫 server block,加入 443 listener 與重新導向。續期由套件安裝的 systemd timer 執行,因此需要驗證兩個元件:systemctl list-timers | grep certbot 可確認 timer 存在,sudo certbot renew --dry-run 可確認續期流程仍能正常運作。完整步驟請參閱在 Ubuntu 24.04 搭配 Nginx 使用 Certbot。如果子網域多到不想逐一列出,同一個工具也能處理透過 DNS-01 challenge 取得 wildcard 憑證。
Traefik 內建 ACME client。 你可以在靜態設定中設定一個 certificate resolver,之後每個 router 都能使用它。所有狀態資料,包括 account key 與憑證,都儲存在單一的 acme.json 檔案中。如果該檔案可被檔案擁有者以外的任何人讀取,Traefik 會拒絕使用,並在移除 resolver 前告知你:
The ACME resolver "le" is skipped from the resolvers list because: unable to get ACME account: permissions 660 for /letsencrypt/acme.json are too open, please use 600掛載一個目錄,讓 Traefik 自行建立檔案。先使用 touch 建立檔案;該檔案會繼承你的 umask,這也是多數人遇到該訊息的原因。
三者都有一點相同。HTTP-01 challenge 需要讓 port 80 可從網際網路連線,因為憑證授權單位會回連該連接埠。只開放 443 時,憑證申請會失敗,而且錯誤看起來像是 DNS 問題。
三種設定中的相同雙應用程式路由工作
工作內容如下:app.example.com 對應至 127.0.0.1:8080 上的服務,files.example.com 對應至 127.0.0.1:8081 上的服務,兩者都使用 HTTPS。以下列出各代理程式中的完整設定,讓冗長程度的差異清楚呈現,而不是只用文字宣稱。
Nginx
# /etc/nginx/sites-available/app.example.com
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}接著建立連結、測試設定、重新載入,並加入憑證。
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d app.example.com執行 nginx -t 以列印 syntax is ok 和 test is successful,這是每次重新載入前都應執行的檢查。第二個應用程式使用相同的區塊,只需修改主機名稱和連接埠。proxy_set_header 行並非裝飾:當 proxy_pass 指定位址時,nginx 預設會將 Host: 127.0.0.1:8080 傳送至上游,因此若應用程式根據 Host 標頭建立絕對 URL,就會把使用者導向 localhost。這 4 個標頭各自的用途,以及在 proxy_pass 結尾加上斜線為何會悄悄改變應用程式收到的路徑,請參閱 nginx server block 指令逐一說明。
Caddy
app.example.com {
reverse_proxy 127.0.0.1:8080
}
files.example.com {
reverse_proxy 127.0.0.1:8081
}sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy這就是完整檔案。reverse_proxy 會自行設定 X-Forwarded-For、X-Forwarded-Proto 和 X-Forwarded-Host,而且預設會忽略用戶端在這些標頭中傳送的內容,因此請求無法向後端謊報來源。憑證、連接埠 80 的重新導向與續期,均由這 2 個網站位址自動處理。檔案中不需要其他設定。
Traefik
Traefik 必須先有靜態設定,才能開始進行路由。以下是 Compose 服務設定,映像標籤截至 August 2026:
services:
traefik:
image: traefik:v3.7
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.email=you@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt接著,每個應用程式在自己的 compose 檔案中,透過 labels 設定專屬路由:
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=le"
- "traefik.http.services.app.loadbalancer.server.port=8080"loadbalancer.server.port 是容器內的連接埠,不是已發布的連接埠,因為 Traefik 會透過共用 Docker network 連線至容器。應用程式完全不需要 ports: 行,這才是實際效益:只有 Traefik 會對外發布。完整建置方式,包括共用 network 和重新導向 middleware,請參閱 使用 Traefik 和 Docker Compose 為多個應用程式設定路由。
每個額外應用程式需要多少設定?
The data behind this chart
[
{
"tool": "Nginx",
"proxy_setup_lines": 0,
"lines_per_app": 11
},
{
"tool": "Caddy",
"proxy_setup_lines": 0,
"lines_per_app": 3
},
{
"tool": "Traefik",
"proxy_setup_lines": 17,
"lines_per_app": 5
}
]根據上方的區塊計算,Nginx server block 有 11 行非空白內容,而且每個主機名稱都要重新撰寫一次。Caddy site block 有 3 行。Traefik 在處理第一個請求前,需要先設定 17 行靜態設定,之後每個應用程式再加入 5 個標籤。
請理解其中的取捨,不要只看誰勝出。Traefik 在加入第一個應用程式前的成本最高,但之後每個應用程式的成本最低;兩者的總成本大約在第 3 個網站時相同。低於這個數量時,靜態設定只是額外負擔。超過這個數量後,標籤的優勢會逐漸增加,因為路由設定與其所路由的服務放在一起。刪除服務時,對應的路由也會一併刪除。這正是集中式設定檔不擅長處理的情況:已停止存在數個月的應用程式,仍留下過時的 server block。
行數也讓 Nginx 看起來較有優勢。每個區塊都需要 symlink、nginx -t、reload 和一次 certbot 執行;Caddy 的修改只需要一次 reload,Traefik 的修改則完全不需要執行命令。三者 reload 時都不會中斷現有連線。差別在於,你凌晨 1 點需要記住多少個分開的步驟。
哪一個元件能辨識你的容器?
Traefik 會監看 Docker socket,並在容器啟動與停止時,根據容器標籤建立路由器。這裡的其他元件都不會自動執行這項工作。Nginx 和 Caddy 都需要在新容器出現時修改設定並重新載入,且必須取得可連線的位址:可以是發佈在 loopback 上的連接埠,也可以是代理已連接的共用 Docker network。
這項功能有其代價,應該明確說明。Traefik 會讀取 /var/run/docker.sock。任何能與該 socket 通訊的人,都能啟動一個將主機檔案系統掛載到容器內的容器,而這等同於取得主機上的 root 權限。以唯讀模式掛載可降低風險,但無法消除風險。如果這符合你的威脅模型,請在中間加入 socket proxy,只公開 Traefik 所需的容器清單端點。
Caddy 可透過社群 plugin 使用以標籤為基礎的服務探索,但 Caddy plugin 會編譯進程式,因此你必須使用 xcaddy 建置自訂 binary 或自訂 image,之後也要自行負責該建置版本及其更新。若只有 3 或 4 個服務,直接編輯 Caddyfile 反而比較省事。
WebSocket 與串流:哪些設定會失效,以及原因
需要調整的是 Nginx。WebSocket 連線一開始是帶有 Upgrade: websocket 的 HTTP 請求,而 nginx 不會將逐跳標頭傳送給上游,除非明確設定。
# /etc/nginx/conf.d/upgrade-map.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}接著,在 location 區塊中,以下 3 行都必須存在:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;如果省略這些設定,瀏覽器主控台會顯示 WebSocket connection to 'wss://app.example.com/ws' failed,而後端日誌會顯示一般的 GET。之所以需要 map,是因為如果寫死 Connection: upgrade,每個請求都會傳送該值,包括應該顯示為 close 的一般請求。
Nginx 的另外 2 個預設值也會造成問題。proxy_read_timeout 為 60 秒,且適用於升級後的通道,因此沒有流量達 1 分鐘的 WebSocket 連線會被代理伺服器關閉。伺服器傳送事件在設定該位置的 proxy_buffering off; 前,會延遲送達或成批出現,因為 nginx 會將回應保留在緩衝區中,而頁面正在等待資料。
Caddy 不需要任何指令,就會執行升級並將連線切換為雙向通道。當回應為 text/event-stream 或長度未知時,Caddy 也會立即 flush,因此不需額外設定即可支援串流。Traefik 會轉送升級,也不會緩衝回應,除非你自行加入其 buffering middleware。如果服務包含聊天室、Web 終端機、日誌即時追蹤或即時儀表板,這會實際影響你需要撰寫及除錯的設定量。
完整的 Nginx server block,包含 WebSocket 與 SSE
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name app.example.com;
client_max_body_size 64m;
location / {
proxy_pass http://127.0.0.1:8080;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_buffering off;
}
}map 應放在 http context 中,而不是 server 內,因此請將它放在 /etc/nginx/conf.d/ 下的獨立檔案中。只有在串流的位置關閉 proxy_buffering,因為對一般回應而言,緩衝能讓 nginx 提早釋放後端 worker。執行 Certbot 時,它會改寫此區塊,因此完成後請再次讀取該檔案。
需要處理不尋常的需求時會發生什麼?
這正是 Nginx 多出那些設定行的價值所在。
- 用戶端憑證,也稱為 mTLS(雙向 TLS),表示用戶端也必須提供憑證。Nginx 需要在 server block 中設定
ssl_client_certificate /etc/ssl/ca.pem;與ssl_verify_client on;。Caddy 則需要在tls內加入client_authblock。Traefik 的 labels 完全無法表達這項設定:您必須在 file provider 中定義 TLS option,再透過traefik.http.routers.app.tls.options=mtls@file將 router 指向該設定。第一次遇到這類需求時,所有設定都放在 labels 中的模式就必須例外處理。 - 大型上傳檔案。 Nginx 預設將 request body 限制為 1 MB。較大的上傳會回傳
413 Request Entity Too Large,而 error log 會記錄client intended to send too large body。請提高client_max_body_size。Caddy 與 Traefik 預設不設定 body limit,因此請求會到達應用程式,再由應用程式自身的限制決定是否接受。 - 回應快取。 Nginx 提供
proxy_cache,而且功能已相當成熟。Caddy 需要編譯包含 plugin 的版本。Traefik 的 open source build 完全沒有 HTTP cache;以為每個 proxy 都會快取的人,通常會對此感到意外。 - 原始 TCP 或 UDP,例如資料庫連接埠或遊戲伺服器。Nginx 提供
streammodule。Traefik 可在各自的 entrypoints 上使用 TCP 與 UDP routers。Caddy 則需要另一個 plugin,因此也需要自訂 build。 - proxy 後方已經有 web server。 如果服務是傳統 PHP 應用程式,Ubuntu 24.04 上的 LAMP stack 已經包含 Apache;在前方再疊加一層 proxy,會有兩個位置設定 headers,也有兩個位置可以重寫 URL。請決定由哪一層執行 TLS termination,然後讓另一層使用綁定至 loopback 的純 HTTP。
這項選擇引發的防火牆陷阱
反向代理的目的,是只開放 80 和 443。Docker 會在不明顯的情況下繞過這項限制。使用 -p 8080:80 發布連接埠時,Docker 會在 nat table 中寫入 DNAT 規則。該規則會先於 ufw 管理的 INPUT 規則套用。因此,ufw deny 8080 無法封鎖這個連接埠,應用程式會直接暴露在公開網際網路上,與你仔細設定的代理並列。請使用 127.0.0.1:8080:80 將已發布的連接埠綁定至 loopback,或完全省略 ports:,讓代理透過 Docker network 連線至容器。上方的 Traefik 範例採用的就是這種方式。相關機制與修正方式請參閱 Docker 發布的連接埠為何會繞過 ufw。
請從不是該 VPS 的機器進行測試,因為直接在該主機上執行的檢查一定會成功:
curl --max-time 5 http://your.server.address:8080你需要看到 Connection refused 或 timeout。若收到 HTTP 回應,表示該應用程式可以不經過代理直接連線。這代表你上方設定的所有內容都只是裝飾。
應該選哪個代理?
以靜態網站為主,另有一兩個應用程式:Caddy。 自動 HTTPS 能免除最主要的例行工作。設定檔短到能在單一畫面中讀完,靜態網站在同一個 site block 中只需一行 root 和一行 file_server。代價是遇到異常時,可直接複製貼上的解法較少。
持續新增服務的 docker-compose homelab:Traefik。 服務超過 3 個後,修改 labels 會比編輯集中式設定檔省事,而且刪除服務時,其路由也會一併移除。第一次設定請預留一個下午,因為 entrypoints、routers、services 和 middlewares 都是新的術語。label 中的拼字錯誤通常會由 Traefik 回傳 404,而不是導致啟動失敗。因此,先查看 docker logs traefik 中的剖析錯誤,再判斷應用程式是否故障。
已有 Nginx 設定,或有上述清單中的任何需求:Nginx。 它已經支援回應快取與用戶端憑證,而且幾乎所有第三方指南都以它為前提。代價是,憑證與 websocket 支援需要自行設定,並非安裝後即可使用。
無論選擇哪一個,都應遵守同一項規則。公開介面上只能有一個程序監聽,其他服務都應監聽 loopback 或私有 Docker network。
FAQ
在同一台 VPS 上執行幾個 Docker 應用程式時,哪個反向代理最好?
如果只有三或四個服務,而且偶爾才新增服務,Traefik 很值得採用,因為每個應用程式都帶有自己的路由標籤,不需要編輯集中式設定檔。如果服務已經穩定,而你主要是不想再處理 HTTPS,Caddy 較容易學習,也較不容易設定錯誤。如果你已經熟悉 Nginx,或需要其他兩者沒有的功能,例如回應快取或純 TCP 監聽器,請選擇 Nginx。
Caddy 真的不需要設定憑證嗎?
一般情況下,是的。只要將公開主機名稱指定為網站位址,就完成全部設定:Caddy 會透過 ACME 申請憑證,在 port 80 提供重新導向,並在憑證到期前續期。仍有兩項條件必須成立。port 80 必須能從網際網路連入,才能完成 HTTP-01 challenge;主機名稱的 DNS A 或 AAAA 記錄也必須已指向該 VPS,因為憑證授權單位會解析該名稱並連回該主機。
我可以在同一台 VPS 上執行 Nginx 和 Traefik 嗎?
不能使用相同的連接埠。後啟動的代理會無法繫結連接埠,nginx 會顯示 bind() to 0.0.0.0:443 failed (98: Address already in use),而 Traefik 會記錄類似的繫結錯誤後結束執行。讓其中一個代理使用 80 和 443,並將其他所有服務置於其後方。如果你正在進行遷移,請一次移動一個主機名稱:在最後一個網站完成移轉前,先讓前端代理將請求轉送到舊代理的 loopback 連接埠。
為什麼我的 websockets 經過 Nginx 後會在 60 秒後中斷?
proxy_read_timeout 的預設值為 60 秒,而且升級完成後會套用至該通道,因此沒有流量持續一分鐘的連線會由代理關閉,而不是由應用程式關閉。請在該 location 上使用 proxy_read_timeout 3600s; 調高逾時時間,或讓應用程式每 30 秒傳送一次 ping frame。Caddy 和 Traefik 不會以一分鐘計時器關閉閒置的升級連線,因此相同的應用程式在這兩者後方可能看起來很穩定,在 Nginx 後方卻不穩定。