SSD Nodes Learn
指南 Matt Connor作者: Matt Connor · 已更新 2026-07-24

Traefik v3 Docker Compose 教學:單一 IP 運行五個應用程式

使用 Traefik v3 與 Docker Compose 實現單一 IP 路由,透過 Host 規則與 Let's Encrypt 自動化 TLS。本文深入解析 Entrypoints、Routers 與 Services 的配置差異,並特別說明如何避免 acme.json 導致的啟動失敗陷阱。

單一 IP、五個應用程式與單一 Port 443

您的 VPS 僅有一個公用 IPv4 位址與單一 TCP Port 443。您希望在該主機上執行 Gitea、應用程式的測試版本 (staging copy)、內部儀表板 (internal dashboard)、狀態頁面 (status page) 以及 Webhook 接收器——即在單一主機上運行五個主機名稱 (hostnames)。反向代理 (reverse proxy) 的作用是佔用 :80 與 :443,讀取每個請求中的 Host 標頭,並將請求轉發至正確的容器。Traefik 具備此功能,且能為每個主機名稱取得並更新憑證,無需手動執行 certbot。

Traefik 與 nginx server {} 區塊的差異在於配置來源。使用 nginx 時,您必須編輯檔案並重新載入,且憑證生命週期仍是一項獨立工作——這正是您 使用 certbot 在 nginx 上核發 Let's Encrypt 憑證 時的流程,其更新定時器完全獨立於 Web Server 之外。Traefik 的 Docker provider 會監控 Docker 事件流,並讀取容器上的 labels:只要啟動帶有 Host() 規則標籤的容器,該容器會在一秒內完成路由設定;停止容器後,路由也會隨之消失。這也是潛在的陷阱。配置若存在於 labels 中,會同時分散在五個地方,且錯誤的標籤不會報錯——容器僅會無法被路由,而 Traefik 不會發出任何錯誤訊息。

四個核心概念

  • Entrypoints 為監聽的 socket。您將定義兩個:web 監聽於 :80,以及 websecure 監聽於 :443
  • Routers 用於比對請求 (Host(...)) 並將其導向至 service。憑證是透過 tls.certresolver 針對每個 router 進行申請。
  • Services 為後端,包含一個 container 以及其在 Docker 網路內監聽的 port。
  • Middlewares 位於 router 與 service 之間:包含基本驗證 (basic auth)、IP 白名單、header 重寫與重新導向 (redirects)。

靜態設定 (entrypoints, providers, ACME) 是透過 Traefik 的 command line 或在 traefik.yml 中傳遞,修改此設定必須重啟 Traefik。動態設定 (routers, services, middlewares) 則來自 container labels 並支援熱載入 (hot-reloaded)。混淆這兩者是導致「flag 無效」的常見原因。

The compose file

One shared Docker network named proxy is the backbone. Traefik reaches a container only if both sit on it.

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, then docker compose logs -f traefik. Each additional app is a copy of the gitea block with its own router name, its own Host() and its own internal port. A Nextcloud install running in Docker with TLS and backups slots in the same way — drop its published ports, attach it to proxy, and let the router labels handle the hostname and the certificate.

Five details there earn their keep.

exposedByDefault=false makes a container invisible to Traefik until it carries traefik.enable=true. Leave it out and every container you ever start — including the throwaway postgres you ran to check something — gets a route generated for it.

providers.docker.network=proxy tells Traefik which network to use when a container is attached to several. Omit it and Traefik may pick the wrong container IP, which surfaces as a 502 that looks like an application fault.

loadbalancer.server.port=3000 is the port inside the container; Gitea listens on 3000 there. Notice that no app container publishes a port at all — only Traefik does.

The redirect on the web entrypoint turns plaintext requests into a 308 to HTTPS. Port 80 stays open anyway: the ACME HTTP challenge needs it, and so do humans who type a bare hostname.

The doubled $$ in the basic-auth hash is Compose escaping, not a typo. Generate it with htpasswd -nbB admin 'your-password' (package apache2-utils), then double every $.

憑證與 acme.json 陷阱

tlschallenge=true 會選用 TLS-ALPN-01:Let's Encrypt 會透過 443 連線至您的主機,Traefik 則在 TLS 握手期間回應挑戰。另一種方式是使用 port 80 的 HTTP-01 — 請將 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 中。若該檔案對群組 (group) 或所有人 (world) 是可讀的,Traefik 會印出與下文極其相似的錯誤訊息並停止運作:

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

正確的解決方案如上所述:將該目錄進行 bind-mount,並讓 Traefik 以正確的權限模式自行建立檔案。如果您使用 touch 建立了 acme.json,您的 umask 會將其權限設為 644。請在主機上修復它:

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

請將該目錄包含在您的應用程式 volume 備份中。遺失該檔案尚可處理 — 憑證可以重新核發 — 但若同時重新核發五個主機名稱,會觸發速率限制 (rate limits)。

在測試階段請使用 staging CA。 取消 caserver 行的註解,確保所有路由皆運作正常,接著將其註解掉並刪除 acme.json,以便重新請求正式版憑證。Let's Encrypt 的正式版對於相同的完整主機名稱組合,每週僅允許核發五次重複的憑證,且會限制針對相同名稱的重複失敗驗證。Staging 會核發不受信任的憑證 — 您的瀏覽器會發出警告,而該警告即代表測試成功 — 且其限制寬鬆許多。

Dashboard 為控制介面,而非演示版本

大多數快速入門指南會設定 --api.insecure=true,這會在 port 8080 啟動 dashboard 且不需驗證。若主機具備公網 IP,任何掃描者都能取得您的路由拓撲、主機名稱、middleware 名稱及 backend ports。

上述 traefik 服務提供了另一種方案:將 dashboard 像其他應用程式一樣進行路由,使用真實主機名稱、透過 TLS 並位於 basicauth 後方。service=api@internal 用於將 router 連接至 Traefik 的內建 API。若要進一步強化安全性,可依序串聯 IP allow-list。若您的辦公室 IP 是動態的,請將範圍設定為 由同台 VPS 自建的 WireGuard VPN 所分配的 subnet,並僅透過該隧道存取 dashboard:

- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth

Docker socket 具備 root 權限

/var/run/docker.sock 是可以建立容器並掛載主機 / 的 API。存取此 API 等同於取得該機器的 root 權限,而 Traefik 需要透過它來讀取 labels。

可以在掛載時保留 :ro,但必須了解其作用:這僅會將 socket 檔案 設為唯讀。這無法阻止 POST 請求透過該 socket 發送到 Docker API。真正的緩解方案是不要直接將 socket 交給 Traefik,而是在兩者之間加入一個過濾代理 (filtering 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 指向該代理:

--providers.docker.endpoint=tcp://dockerproxy:2375

Traefik 將保留對 containers 與 networks 的讀取權限,並失去建立任何資源的能力。

防火牆、連接埠與常見錯誤的規則

兩個開啟的連接埠,加上 SSH:

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

Docker 的已發佈連接埠會繞過 ufw。 Docker 會插入自身的 iptables 規則,且其優先權高於 ufw 的鏈(chains)。因此,即使 ufw 設定了 deny,使用 ports: ["3000:3000"] 啟動的容器仍可從網際網路存取。這屬於結構性問題,而非防火牆設定錯誤:請僅從 Traefik 發佈連接埠,並為其他所有容器提供 networks: [proxy] 且不提供其他權限。若有必要讓外部存取主機,請將其綁定至 loopback — "127.0.0.1:3000:3000"

Troubleshooting: errors you will actually see

404 page not found,由 Traefik 傳回。沒有匹配的 router。按發生機率排序:container 缺少 traefik.enable=true(且未設定 exposedByDefault=false);Host() rule 與您輸入的名稱不符;一個 label 中的 router 名稱與另一個不同(routers.gitea.rulerouters.gitea.entrypoints 必須為相同字串);或者您在 hostname 使用了引號而非 backticks。Traefik v3 的 matcher 內部必須使用 backticks。

502 Bad Gateway。router 已匹配但無法連線至 backend。通常是因為 container 不在 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。該 hostname 沒有對應的憑證,因此 Traefik 傳回其自簽章的 placeholder。請閱讀 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/...:外部無法透過 port 80 連線至 Traefik — 通常是 VPS 前端的供應商層級防火牆,而非 ufw。

憑證無法核發,且您的 DNS 使用 Cloudflare 並開啟了橘色雲朵。Cloudflare 會在邊緣端終止 TLS,導致 TLS-ALPN-01 無法通過。核發期間請將紀錄設為 DNS-only,或改用具備 API token 的 DNS-01 challenge。DNS-01 也是唯一能核發 wildcard 憑證的 challenge。

Redirect loop。Traefik 前端已有機制終止 TLS 並將明文轉發至 :80;entrypoint 的 redirect 又將其轉回 HTTPS。請移除這兩個 redirect 中的其中一個。

保持運行

Docker 的 unit 必須設定為開機啟動 (systemctl is-enabled docker),且 restart: unless-stopped 會在重新啟動後恢復 stack。若需明確控制,可執行一個運行 docker compose -f /srv/edge/compose.yml up -d 並帶有 RemainAfterExit=yes 的小型 systemd unit,以獲得 systemctl status edge 與順序控制。

請固定 Traefik 的 tag (使用 traefik:v3.5,絕不要使用 latest)。從 v2 升級至 v3 時,rule syntax 與 provider 名稱皆已變更,若未經人工干預進行 latest,系統會嘗試重新載入無法辨識的 config。請謹慎升級:閱讀遷移說明、更新 tag、執行 docker compose up -d traefik 並觀察 log。若您仍在使用 v2 tag,請參閱 Traefik v2 升級至 v3 遷移指南,其中詳細說明了所有重新命名規則、相容模式,以及可保留憑證的 rollback 流程。

請備份 ./letsencrypt 與每個 app 的 data volume。除了這些,Traefik 不持有任何無法從 compose file 重建的 state。

擴展規模時的限制

第一個瓶頸並非吞吐量,而是單一主機:在單一 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,或是將 access log 寫入檔案並進行輪轉(rotate)。

上述操作皆不需要編排器(orchestrator)。但你需要一台由你控制的伺服器,具備真實 IP 並對外開放 80 與 443 埠號 —— 僅需一台小型 VPS 即可滿足所有依賴條件。

FAQ

如果我已經執行 Traefik,還需要使用 certbot 嗎?

不需要。Traefik 的 ACME resolver 會為每個路由的主機名稱請求並更新憑證,並將結果儲存在 acme.json。若使用 nginx 或其他伺服器自行處理 TLS,則 Certbot 仍是正確的工具;在同一個主機名稱上同時執行兩者,會導致 Let's Encrypt 的速率限制 (rate limits) 被耗盡。

為什麼我的容器透過 Traefik 回傳 404?

Traefik 回傳 404 表示沒有任何 router 匹配該請求。請檢查容器是否帶有 traefik.enable=true(一旦設定了 exposedByDefault=false 則為強制要求)、Host() 的值是否與您輸入的名稱相符,以及該應用程式的所有 label 中的 router 名稱是否完全一致。Traefik v3 的 matcher 內部需使用反引號 (backticks) 而非引號。

這裡的 404 與 502 有何區別?

404 表示路由未觸發;502 表示 router 已匹配,但後端拒絕連線。常見的 502 原因包括:容器未連接至 proxy 網路,或是 loadbalancer.server.port 指向了已發佈的 port,而非應用程式在容器內監聽的 port。存取日誌 (access log) 會記錄 Traefik 嘗試連線的確切位址。

以唯讀方式掛載 Docker socket 是否足夠?

:ro 旗標僅將 socket 檔案設為唯讀,而非其背後的 API —— POST 請求仍會透過它傳輸,且存取 Docker API 等同於擁有主機的 root 權限。更安全的做法是使用上方所示的 docker-socket-proxy 容器,它僅向 Traefik 開放容器與網路的讀取權限,並完全封鎖寫入操作。

Traefik 可以核發萬用憑證 (wildcard certificate) 嗎?

僅能透過 DNS-01 驗證方式,且需提供 DNS 供應商的 API token。TLS-ALPN-01 與 HTTP-01 僅能驗證單一主機名稱,無法產生萬用憑證。若使用 Cloudflare 等 CDN 在您的 VPS 前端處理 TLS,且其他兩種驗證方式皆無法完成時,DNS-01 是唯一的解決方案。