SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-30

Traefik Docker Compose 設定:5 個應用共用一個 IP

用 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.5
    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 安裝也採用相同方式:移除其公開埠號,將它連接到 proxy,再由 router labels 處理主機名稱與憑證。

其中有 5 個細節值得特別注意。

exposedByDefault=false 會讓容器在設定 traefik.enable=true 前對 Traefik 隱藏。若省略這項設定,之後啟動的所有容器,包括用來暫時檢查問題的 postgres,都會自動產生路由。

providers.docker.network=proxy 會告訴 Traefik 在容器連接到多個 network 時應使用哪一個。若省略此設定,Traefik 可能選到錯誤的容器 IP,結果會顯示為 502,看起來像是應用程式故障。

loadbalancer.server.port=3000 是容器內部的埠號;Gitea 在該處監聽 3000。請注意,沒有任何應用程式容器發布埠號,只有 Traefik 會發布埠號。

web entrypoint 上的重新導向,會將純文字請求轉換為 HTTPS 的 308。80 埠仍須保持開放:ACME HTTP challenge 需要使用它,使用者直接輸入主機名稱時也需要使用它。

basic-auth hash 中重複的 $$ 是 Compose 的跳脫語法,不是錯字。使用 htpasswd -nbB admin 'your-password' 產生它(套件為 apache2-utils),然後將每個 $ 加倍。

憑證與 acme.json 陷阱

tlschallenge=true 會選用 TLS-ALPN-01:Let’s Encrypt 會連線到伺服器的 443 埠,而 Traefik 會在 TLS 交握期間回應挑戰。另一個選項是使用 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.json 中保存 ACME 帳戶金鑰與所有已簽發的憑證。如果該檔案可供群組或所有使用者讀取,Traefik 會輸出與以下內容非常接近的訊息,然後停止:

error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600

最乾淨的修正方式就是前面所述的方法:繫結掛載目錄,讓 Traefik 自行以正確的模式建立檔案。如果你使用 touch 建立了 acme.json,umask 會使其權限變成 644。請在主機上修正:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

請將該目錄與應用程式的 volume 一起備份。遺失它仍可復原,因為憑證會重新簽發;但同時為五個主機名稱重新申請憑證,可能會觸及速率限制。

在反覆測試期間使用 staging CA。 取消註解 caserver 行,確認每個路由都能運作後,再將該行註解並刪除 acme.json,讓系統重新申請正式環境憑證。正式環境的 Let’s Encrypt 對完全相同主機名稱集合的 duplicate 憑證,每週允許 5 張;相同名稱的驗證若持續失敗,也會受到節流限制。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:2375

Traefik 仍可讀取容器與網路,但失去建立任何資源的能力。

防火牆、連接埠,以及大家都會弄錯的規則

開放 2 個連接埠,另加 SSH:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Docker 發布的連接埠會繞過 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.rulerouters.gitea.entrypoints 必須是相同的字串);或您以引號包住主機名稱,而不是使用反引號。Traefik v3 的 matcher 內必須使用反引號。

502 Bad Gateway。 router 已符合條件,但無法連線到後端。幾乎都是因為容器不在 proxy network 上,請檢查 docker inspect -f '{{json .NetworkSettings.Networks}}' gitea。另一種可能是 loadbalancer.server.port 錯誤:您填入了 published 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.com

DNS 尚未指向這台主機。修正記錄,等待 TTL 到期,再重新啟動 Traefik。

HTTP challenge 顯示 Invalid response from http://git.example.com/.well-known/acme-challenge/... 從外部連至 Traefik 的 port 80 不通。通常是 VPS 前方的供應商層級防火牆,而不是 ufw。

憑證始終無法核發,且 DNS 使用 Cloudflare 並開啟橘色雲朵。 Cloudflare 會在其 edge 終止 TLS,因此 TLS-ALPN-01 無法透過 Cloudflare 完成。核發憑證期間,將記錄設為 DNS-only;或改用搭配 API token 的 DNS-01 challenge。DNS-01 也是唯一能核發 wildcard 憑證的 challenge。

重新導向迴圈。 Traefik 前方的元件已終止 TLS,並將純文字流量轉送至 :80;entrypoint redirect 又將其轉回 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.5,不要使用 latest)。從 v2 升級至 v3 時,規則語法與 provider 名稱已變更;無人管理的 latest 可能會直接重新載入已無法解析的設定。請有計畫地升級:閱讀遷移說明、更新 tag、docker compose up -d traefik,並監控日誌。若仍使用 v2 tag,Traefik v2 到 v3 遷移指南會逐一說明名稱變更、相容性模式,以及可保留憑證的復原方式。

備份 ./letsencrypt 與每個應用程式的資料 volume。Traefik 不會保存其他無法從 compose file 重建的狀態。

大規模運作時會在哪裡失效

第一個瓶頸不是吞吐量,而是單一主機:一台 VPS 上只有一個 Traefik,會成為五個應用程式的單一故障點;而 acme.json 是平面檔案儲存,兩個 Traefik 執行個體同時寫入會造成資料損毀。要向外擴展,就必須將憑證儲存移出檔案,或改在其他位置終止 TLS。

第二個問題是長時間維持的連線。Server-sent events、大型上傳和回應緩慢的用戶端,會受到 entrypoint 的回應逾時設定影響;--entryPoints.websecure.transport.respondingTimeouts.readTimeout 及其 writeTimeoutidleTimeout 同類設定就是調整這些行為的參數。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 旗標會讓 socket 檔案變成唯讀,但不會限制其後的 API;POST 請求仍會透過該 socket 傳送,而存取 Docker API 等同於取得主機的 root 權限。較安全的做法是使用上文所示的 docker-socket-proxy 容器。它只向 Traefik 提供容器與 network 的讀取功能,並完全封鎖寫入操作。

Traefik 可以簽發萬用字元憑證嗎?

只能使用 DNS-01 challenge,並提供 DNS 供應商的 API token。TLS-ALPN-01 與 HTTP-01 都只能驗證單一主機名稱,無法產生萬用字元憑證。當 Cloudflare 等 CDN 在 VPS 前方終止 TLS,且另外兩種 challenge 始終無法完成時,也應使用 DNS-01。