Traefik Docker Compose 反向代理 5 個應用程式
使用 Traefik v3 與 Docker Compose,透過 Host 規則讓 5 個應用程式共用一個 IP,自動申請 Let's Encrypt TLS,並避開 acme.json 權限錯誤導致憑證無法簽發。
單一 IP、5 個應用程式、單一 443 埠
您的 VPS 只有一個公開 IPv4 位址和一個 TCP 443 埠。您想在同一台主機上執行 Gitea、應用程式的 staging 副本、內部儀表板、狀態頁面和 webhook 接收器,使用 5 個主機名稱。反向代理程序會接管 :80 和 :443,讀取每個請求中的 Host標頭,並將請求交給正確的容器。Traefik 會執行這項工作,並為每個主機名稱申請及續期憑證,您不必手動執行 certbot。Nginx 和 Caddy 同樣能妥善處理這 5 個主機名稱。因此,如果您尚未決定,將所有服務接到其中一個代理之前,值得先比較這 3 個代理在憑證處理方式和每個應用程式的設定成本。
Traefik 與 nginx server {} 區塊的差異,在於設定來源不同。使用 nginx 時,您編輯檔案並重新載入;憑證生命週期則是另一項工作。這就是您在使用 certbot 在 nginx 上申請 Let’s Encrypt 憑證時所採用的流程,續期計時器完全由 Web 伺服器之外的元件管理。Traefik 的 Docker provider 會監看 Docker 事件串流,並讀取容器上的 labels:啟動帶有 Host() 規則 label 的容器後,該容器會在 1 秒內可供路由;停止容器後,該路由就會消失。這也是其中的陷阱。設定分散在 5 個位置的 labels 中,錯誤的 label 不會產生明確錯誤;容器只是不會被路由,而 Traefik 不會顯示任何訊息。
四個名詞
- Entrypoints 是監聽中的 socket。您將定義兩個:
web位於:80,以及websecure位於:443。 - Routers 會比對請求(
Host(...)),並將請求連接至服務。憑證會透過tls.certresolver,以每個 router 為單位申請。 - Services 是後端,包含一個容器,以及該容器在 Docker network 內部 監聽的連接埠。
- Middlewares 位於 router 與 service 之間:基本驗證、IP allow-list、標頭重寫及重新導向。
這四個名詞是 Traefik 對應手動撰寫之設定的名稱:router 是 server_name,service 是 proxy_pass 目標,而 middlewares 則對應您在 逐行建立 nginx 反向代理伺服器區塊 時自行設定的標頭與驗證指令。
靜態設定(entrypoints、providers、ACME)會透過 Traefik 的命令列或 traefik.yml 傳入;變更後必須重新啟動 Traefik。動態設定(routers、services、middlewares)則來自容器 labels,並會自動重新載入。混淆兩者,是最常導致「我的 flag 沒有作用」的原因。
Compose 檔案
名為 proxy 的共用 Docker network 是整體架構的核心。Traefik 只有在容器與自己都連接到該 network 時,才能連線到容器。
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.7
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.network=proxy
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
# while you iterate, point at staging so a mistake costs nothing:
# - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --api.dashboard=true
- --log.level=INFO
- --accesslog=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=le
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS
gitea:
image: gitea/gitea:1 # major-only pin keeps this demo copy-pasteable; pin an exact release in production
restart: unless-stopped
volumes:
- ./gitea:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000先設定 docker compose up -d,再設定 docker compose logs -f traefik。每個額外的應用程式都複製 gitea 區塊,並使用專屬的 router 名稱、Host() 與內部連接埠。在 Docker 中執行並搭配 TLS 與備份的 Nextcloud 安裝也採用相同方式:移除其 published ports,將它連接到 proxy,再讓 router labels 處理主機名稱與憑證。
其中有 5 個細節值得注意。
exposedByDefault=false 會讓容器在帶有 traefik.enable=true 之前對 Traefik 不可見。若省略此設定,之後啟動的每個容器都會被建立路由,包括你為了檢查某項功能而暫時啟動的 postgres。
providers.docker.network=proxy 會在容器同時連接多個 network 時,告訴 Traefik 應使用哪個 network。若省略此設定,Traefik 可能選到錯誤的容器 IP,並呈現為 502,表面上看起來像是應用程式故障。
loadbalancer.server.port=3000 是容器內部的連接埠;Gitea 會在該處監聽 3000。請注意,沒有任何應用程式容器發布連接埠,只有 Traefik 會發布連接埠。
web entrypoint 上的重新導向預設為永久重新導向,因此純文字 GET(瀏覽器送出的請求)會收到指向 HTTPS 的 301;Traefik 只會對 POST 等其他方法回應 308,讓方法在重新導向後保留。連接埠 80 仍會保持開啟:ACME HTTP challenge 需要使用該連接埠,而直接輸入主機名稱的使用者也需要使用它。
basic-auth hash 中重複的 $$ 是 Compose 的跳脫語法,不是錯字。請使用 htpasswd -nbB admin 'your-password'(套件 apache2-utils)產生 hash,然後將每個 $ 加倍。
憑證與 acme.json 陷阱
tlschallenge=true 會選用 TLS-ALPN-01:Let's Encrypt 會連線到您的主機 443 埠,Traefik 則在 TLS handshake 期間回應 challenge。另一個選項是 HTTP-01,使用 80 埠。請將 Traefik 的 command: 清單中的 tlschallenge 行替換為以下兩行:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web兩者皆可使用。兩者都要求主機名稱的公開 DNS 已指向您的 VPS,且憑證授權單位能解析該名稱並從外部連線。先建立 A(以及 AAAA)記錄,使用 dig +short git.example.com 確認,再啟動 Traefik。
接下來是讓許多人浪費一晚時間的陷阱。Traefik 會將 ACME 帳戶金鑰與所有已簽發的憑證儲存在單一 acme.json 中。如果該檔案可供群組或所有使用者讀取,Traefik 會記錄一行與以下內容非常接近的訊息,並停用該 resolver。Traefik 仍會執行並轉送流量,但不會申請任何憑證,因此每個主機名稱都會提供 TRAEFIK DEFAULT CERT:
ERR The ACME resolve is skipped from the resolvers list error="unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600" resolver=le正確的修正方式就是上方所述:繫結掛載 目錄,讓 Traefik 自行以正確模式建立檔案。如果您使用 touch 建立了 acme.json,umask 會使其權限變成 644。請在主機上修正:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefik請將該目錄與應用程式的 volume 一起備份。遺失一次仍可復原,憑證會重新簽發;但每次重新部署都清除此目錄,會導致您觸發每 7 天針對完全相同主機名稱集合最多簽發五張憑證的限制。
在反覆測試期間使用 staging CA。 取消註解 caserver 行,確認所有路由都能正常運作後,再將該行註解並刪除 acme.json,讓系統重新申請 production 憑證。Production Let's Encrypt 對完全相同的主機名稱集合,每週允許簽發五張 duplicate 憑證,並會限制針對相同名稱反覆驗證失敗的請求。Staging 會簽發不受信任的憑證,瀏覽器會顯示警告;這項警告代表流程已成功,而且 staging 的限制寬鬆得多。
Dashboard 是控制介面,不是展示用的示範
大多數快速入門設定都會設置 --api.insecure=true,讓 Dashboard 在 8080 埠提供服務,且未啟用驗證。若伺服器具有公開 IP,任何進行掃描的人都能取得你的路由拓撲、主機名稱、中介軟體名稱與後端埠號。
上方 traefik 服務的標籤提供另一種做法:像其他應用程式一樣,透過真正的主機名稱及 TLS 路由 Dashboard,並置於 basicauth 後方。service=api@internal 會將路由器連接到 Traefik 內建的 API。你也可以依序串接 IP allow-list,進一步限制存取。如果辦公室的 IP 位址是動態的,請將範圍設為 你在同一台 VPS 上自行託管的 WireGuard VPN 所分配的子網路,並只透過通道存取 Dashboard:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth當 5 個應用程式都需要各自的帳戶時,共用一組 basicauth 密碼就不再合理。同一個 middleware 插槽也可以改用 forwardauth,將驗證決策交給 Authentik,一部自架的單一登入伺服器。如此一來,Dashboard 及其旁的每條路由都只需經過一次登入,並可在單一位置撤銷存取權。
Docker socket 具有 root 權限
/var/run/docker.sock 是一個 API,可建立容器,並掛載主機上的 /。取得它的存取權,等同於取得該機器的 root 權限;Traefik 需要使用它來讀取 labels。
保留掛載上的 :ro,但必須清楚說明它的作用:它只會讓 socket 檔案 成為唯讀。它不會阻止透過該 socket 傳送 POST 要求至 Docker API。真正的緩解方式,是不要將 socket 交給 Traefik,並在中間加入可過濾要求的 proxy:
dockerproxy:
image: tecnativa/docker-socket-proxy # pin the current tag
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy移除 Traefik 的 socket volume,並將 provider 指向 proxy:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik 仍可讀取容器與網路,但失去建立任何資源的能力。
防火牆、連接埠,以及大家都會弄錯的規則
開放 2 個連接埠,另加 SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableDocker 發布的連接埠會繞過 ufw。 Docker 會插入自己的 iptables 規則,並在 ufw 鏈之前先進行評估。因此,即使 ufw 已在相應位置設定 deny,使用 ports: ["3000:3000"] 啟動的容器仍可從網際網路連線。防禦方式在於架構,而不是防火牆設定:只允許 Traefik 發布連接埠,其他每個容器都只設定 networks: [proxy]。如果確實有服務必須連到主機,請將它繫結至 loopback,使用 "127.0.0.1:3000:3000"。
疑難排解:實際會遇到的錯誤
404 page not found,由 Traefik 提供。沒有任何 router 符合。依常見程度排列:容器缺少 traefik.enable=true(且已設定 exposedByDefault=false);Host() 規則不符合您輸入的名稱;某個 label 中的 router 名稱與另一個 label 不同(routers.gitea.rule 和 routers.gitea.entrypoints 必須是相同的字串);或您以單引號包住了主機名稱。Traefik v3 在 matcher 中接受反引號或跳脫的雙引號(\"),不接受單引號。
502 Bad Gateway。 Router 已符合,但無法連線至後端。幾乎都是因為容器不在 proxy network 上,請檢查 docker inspect -f '{{json .NetworkSettings.Networks}}' gitea。另一個可能原因是 loadbalancer.server.port 錯誤:您提供了已發布的 port,或應用程式在其他位置監聽。日誌會指出連線嘗試:dial tcp 172.18.0.5:8080: connect: connection refused。
瀏覽器顯示警告,且憑證簽發給 TRAEFIK DEFAULT CERT。 該主機名稱沒有對應的憑證,因此 Traefik 提供了自簽名的預設憑證。查看 ACME 訊息:
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comDNS 尚未指向該主機。修正記錄,等待 TTL 到期,再重新啟動 Traefik。
HTTP challenge 顯示 Invalid response from http://git.example.com/.well-known/acme-challenge/...:從外部連入的 port 80 未到達 Traefik。通常是 VPS 前方的供應商層級防火牆造成,而不是 ufw。
憑證始終無法簽發,且 DNS 使用 Cloudflare 並啟用橙色雲朵。 Cloudflare 會在其 edge 終止 TLS,因此 TLS-ALPN-01 無法穿透完成驗證。簽發憑證期間,將記錄設為 DNS-only;或改用具有 API token 的 DNS-01 challenge。DNS-01 也是唯一能簽發萬用字元憑證的 challenge。
重新導向迴圈。 Traefik 前方的某個元件已先終止 TLS,並將純文字流量轉送至 :80;entrypoint 的重新導向又將其導回 HTTPS。請移除其中一個重新導向。
維持服務執行
Docker 的 unit 必須設為開機啟用(systemctl is-enabled docker),而 restart: unless-stopped 會在重新開機後恢復 stack。若需要明確管理,可建立小型 systemd unit,使用 docker compose -f /srv/edge/compose.yml up -d 搭配 RemainAfterExit=yes,以取得 systemctl status edge 與啟動順序控制。
固定 Traefik tag(traefik:v3.7,不要使用 latest),並固定在仍會發布版本的分支。截至 2026-10-12,Traefik 列出的受支援 v3 分支只有 v3.7。v3.5 已在 7 November 2025 發布最後一個版本 v3.5.6,而且其 Docker provider 將 API version 固定為 1.24。Docker Engine 29 及後續版本會以 client version 1.24 is too old 拒絕此版本,因此不會有任何 container 能完成路由。Traefik 3.6.1 加入 Docker API version negotiation,因此上方的 compose file 使用 v3.7。從 v2 升級至 v3 時,rule syntax 與 provider 名稱都已變更;無人值守的 latest 會直接重新載入它已無法理解的設定。請有計畫地升級:閱讀 migration notes、更新 tag、執行 docker compose up -d traefik,並監看 log。如果仍在使用 v2 tag,Traefik v2 至 v3 migration guide 會逐一說明名稱變更、相容模式,以及可保留憑證的 rollback 方式。
備份 ./letsencrypt 與每個 app 的 data volume。Traefik 不會保存其他無法從 compose file 重建的狀態。
大規模運作時會在哪裡失效
第一個瓶頸不是吞吐量,而是單一主機:一台 VPS 上只有一個 Traefik,會成為五個應用程式的單一故障點;而 acme.json 是平面檔案儲存,兩個 Traefik 執行個體同時寫入會造成資料損毀。要向外擴展,就必須將憑證儲存移出檔案,或改在其他位置終止 TLS。
第二個問題是長時間維持的連線。Server-sent events、大型上傳和回應緩慢的用戶端,會受到 entrypoint 的回應逾時設定影響;--entryPoints.websecure.transport.respondingTimeouts.readTimeout 及其 writeTimeout、idleTimeout 同類設定就是調整這些行為的參數。WebSockets 可直接通過,不需要額外設定。
第三個問題是磁碟。--accesslog=true 將內容寫入 stdout,而 Docker 的 json-file driver 會永久保留這些內容,除非設定容量上限。在 Traefik 服務上設定 logging.options.max-size,或將存取日誌寫入檔案並進行輪替。
這些情況都不需要 orchestrator。但你需要一台可控制的伺服器,具備實際 IP,且 80 和 443 埠對外開放;一台小型 VPS 就是完整的相依項目清單。
FAQ
我使用 Traefik 時,仍需要 certbot 嗎?
不需要。Traefik 的 ACME resolver 會為它路由的每個主機名稱申請並續期憑證,並將所有憑證儲存在 acme.json。當 nginx 或其他伺服器自行終止 TLS 時,certbot 仍是適合的工具;讓兩者同時處理相同的主機名稱,只會消耗 Let’s Encrypt 的速率限制。
為什麼我的容器透過 Traefik 會回傳 404?
Traefik 提供的 404 表示完全沒有 router 符合該請求。請確認容器具有 traefik.enable=true(設定 exposedByDefault=false 後即為必要項目)、Host() 的值符合您輸入的名稱,且該應用程式所有 label 使用的 router 名稱完全一致。Traefik v3 也要求 matcher 內使用反引號或跳脫的雙引號;單引號會被拒絕。
這裡的 404 與 502 有何差異?
404 表示尚未進行路由;502 表示已有 router 符合,但後端拒絕連線。常見的 502 原因包括容器未連接至 proxy network,以及 loadbalancer.server.port 指向已發布的連接埠,而不是應用程式在容器內監聽的連接埠。存取日誌會列出 Traefik 實際連線的確切位址。
將 Docker socket 以唯讀方式掛載就足夠了嗎?
:ro flag 只會讓 socket 檔案變成唯讀,不會限制其背後的 API;POST 請求仍會透過它傳送,而 Docker API 存取權等同於主機上的 root 權限。更嚴格的作法是使用上方所示的 docker-socket-proxy container;它只向 Traefik 暴露容器與 network 的讀取功能,並直接封鎖寫入。
Traefik 可以簽發 wildcard 憑證嗎?
只有透過 DNS-01 challenge,並使用 DNS provider 的 API token。TLS-ALPN-01 與 HTTP-01 都各自驗證單一主機名稱,無法產生 wildcard。當 Cloudflare 等 CDN 在 VPS 前方終止 TLS,而另外兩種 challenge 始終無法完成時,DNS-01 也是可行的選項。