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

Docker Compose 用 Tailscale Sidecar 私有化服務

Tailscale 官方 Compose 範例會留下公開 8080 埠。改用 sidecar 共用 network namespace,移除 published port,僅透過 tailnet 名稱存取應用程式。

在 Docker Compose 堆疊中執行 Tailscale

在 Docker Compose 中執行 Tailscale 需要 2 個服務。其中一個是會加入 tailnet 的 tailscale/tailscale 容器。另一個是應用程式容器。它會共用前一個容器的 network namespace,而不是在主機上發布連接埠。如此一來,您的筆電可以透過名稱開啟該服務,而公開網際網路完全無法連入。

tailnet 是 Tailscale 在您登入的裝置之間建立的私有網路。如果您不熟悉這個詞,請先閱讀Tailscale 的功能,以及它如何連接兩台機器。本指南假設 Docker Engine 與 Compose v2 plugin 已可正常運作,並且已依照在 VPS 上建立第一個 Docker Compose 堆疊完成設定。

廠商範例,以及它留下的開放連接埠

Tailscale 自己的 Compose 指南提供了與此接近的堆疊。

services:
  tailscale:
    image: tailscale/tailscale:latest
    container_name: tailscale
    hostname: tailscale-nginx
    environment:
      - TS_AUTHKEY=tskey-auth-REPLACE-ME
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ./tailscale-state:/var/lib/tailscale
    cap_add:
      - net_admin
      - net_raw
    restart: unless-stopped

  nginx:
    image: nginx:latest
    container_name: nginx_server
    ports:
      - "8080:80"
    depends_on:
      - tailscale
    restart: unless-stopped

tailscale 服務中的每一行都正確。TS_AUTHKEY 會驗證節點。TS_STATE_DIR 會告訴 tailscaled 將狀態寫入何處,而 bind mount 會將該狀態保留在磁碟上。問題出在第二個服務。

這兩個容器位於預設的 Compose bridge network,各自擁有自己的位址。這是一般行為,Compose network 如何依服務名稱連接容器 中已有說明。tailscale 容器只為自身加入 tailnet,沒有將任何流量轉送到 nginx 容器。因此,連線到 nginx 的唯一方式是使用主機上的 8080 埠。

除非在連接埠前指定位址,否則 published port 會繫結至 0.0.0.0。因此在 VPS 上,該連接埠會透過公開 IP 回應。應用程式名義上位於 tailnet,實際上卻暴露在網際網路上。主機防火牆也無法解決這個問題,因為 Docker 會在 ufw 的 chain 之前插入自己的轉送規則。為什麼 ufw deny 規則無法關閉 published Docker port 說明了這個陷阱。

還有一個值得指出的細節。範例授予 net_admin 和 net_raw,但從未對應 /dev/net/tun。TS_USERSPACE 預設為 true,因此容器會執行 userspace network stack,而這兩項 capability 不會發揮作用。

Sidecar:共用網路命名空間,不發布連接埠

使用 network_mode: service:tailscale 將應用程式放入 tailscale 容器的網路命名空間。如此一來,兩個程序會看到相同的 loopback 與相同的 tailnet 位址,雖然它們仍在不同的容器中執行。

services:
  tailscale:
    image: tailscale/tailscale:v1.102.3
    container_name: ts-nginx
    hostname: nginx-demo
    environment:
      - TS_AUTHKEY=${TS_AUTHKEY}
      - TS_HOSTNAME=nginx-demo
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ./ts-state:/var/lib/tailscale
    restart: unless-stopped

  nginx:
    image: nginx:1.30.4-alpine
    network_mode: service:tailscale
    depends_on:
      - tailscale
    restart: unless-stopped

金鑰應放在 compose 檔案旁的 .env 檔案中,而不是寫入 YAML。這樣提交的檔案就不會包含 secret。檔案只需一行:TS_AUTHKEY=tskey-auth-...。避免將 secret 放入已提交的 compose 檔案會說明此模式的其餘部分。

啟動服務,並檢查兩個部分。

docker compose up -d
docker compose exec tailscale tailscale status
docker compose logs --tail 20 tailscale

tailscale status 應列出此節點及其 100.x 位址,接著列出 tailnet 上的其他機器。在同樣已登入的筆電上,curl http://nginx-demo/ 應回傳 nginx 歡迎頁面。在 VPS 本機上,sudo ss -lntp | grep 8080 不會回傳任何內容,因為沒有發布連接埠。

為何使用 port 80,而不是 8080:在 userspace mode 中,tailscaled 會將傳入的 tunnel 連線轉送到 localhost 上相同的 port。nginx 在共用的命名空間內監聽 port 80,因此 tailnet 會透過 port 80 存取它。若變更應用程式監聽的 port,tailnet 使用的 port 也會隨之變更。這種共用命名空間的技巧不僅適用於 Tailscale;如何連線到主機及 stack 其餘部分,也會遇到相同問題,詳見由 Gluetun 容器管理鄰近容器網路的架構。

容器需要哪一種驗證金鑰?

金鑰類型會決定第二次啟動時的行為,因此請在部署前選定。請在管理主控台的 Keys 頁面產生金鑰。對話方塊只會顯示一次。

  • One-off keys 可驗證單一裝置。若重建 stack 時未保留其狀態目錄,stack 將無法恢復。
  • Reusable keys 可驗證不限數量的裝置。Compose stack 通常需要這種金鑰。
  • Ephemeral keys 會將節點標記為自動清理。Tailscale 會在 ephemeral 裝置最後一次活動後 30 to 60 分鐘移除該裝置。
  • Pre-approved keys 可略過手動核准裝置的步驟。這只有在 tailnet 啟用裝置核准時才有影響。
  • Tagged keys 會在驗證時套用 ACL 標籤,例如 tag:container。裝置不再隸屬於個人,且其金鑰預設會停用到期機制。

最後一點是營運上的關鍵。節點金鑰預設會在 180 days 後到期;到期的節點會從 tailnet 移除,直到有人再次為它完成登入。Tagged key 可移除此到期提醒,因此標籤適合用於伺服器與容器。

驗證金鑰到期是另一個事件,兩者很容易混淆。驗證金鑰的有效期為 1 to 90 days,預設為 90。金鑰到期不會中斷已完成驗證的裝置,只會停止新增裝置。對於長期執行的服務,請使用可重複使用、具標籤且非 ephemeral 的金鑰。對於經常拆除的 stack,例如預覽環境,ephemeral key 可讓管理主控台保持整潔,無須手動刪除裝置。

為什麼容器會以新機器的身分重新出現?

因為 tailscaled 將狀態寫入容器的可寫入層,而 docker compose down 刪除了容器。

節點身分儲存在該狀態目錄中。持續保存這個目錄後,容器在重新啟動時會保留其名稱與 100.x 位址,也會保留服務設定。若遺失該目錄,下次啟動就會視為首次啟動:容器會使用相同的金鑰重新進行驗證,而管理主控台會新增第二台機器。兩者都宣告主機名稱 nginx-demo,因此 MagicDNS 會在較新的節點名稱後加上數字後綴,而你儲存的所有連結都會指向已失效的節點。

兩項條件都必須成立。必須設定 TS_STATE_DIR=/var/lib/tailscale,因為在 Kubernetes 外部它沒有預設值。此外,該路徑必須掛載,可以使用上方的繫結掛載或具名磁碟區;兩者的選擇請參閱 繫結掛載與具名磁碟區的比較。只設定其中一項是最常見的錯誤,且通常不會立即顯示錯誤:堆疊會正常運作,直到第一次 down。

請進行驗證,不要只是假設設定正確。

docker compose down
ls -l ./ts-state
docker compose up -d
docker compose exec tailscale tailscale status

在第二次 up 前,./ts-state 應已包含 tailscaled.state,且節點重新出現時應保留原本的位址。若位址不同,表示掛載未正常運作。

固定映像,並記錄使用的標籤

tailscale/tailscale:latest 會跟隨最新的穩定版本。六個月後,docker compose pull 可能會在無提示的情況下將 tailscaled 替換成其他版本,下一次重新啟動時便會執行你從未選擇的程式碼。上方的堆疊已固定使用 v1.102.3,也就是截至 2026 年 9 月的穩定版本。Docker Hub 也會針對修補程式版本發布 v1.102,以及你不應在伺服器上使用的 unstable 標籤。

請刻意進行升級。

docker compose pull tailscale
docker compose up -d
docker compose exec tailscale tailscale version

編輯標籤並執行 up -d 會重新建立容器,這與重新啟動容器不同。在偵錯未生效的版本變更前,建議先閱讀重新啟動、啟動與重新建置的差異。

使用者空間網路及其代價

TS_USERSPACE 預設為 true。容器接著會在使用者空間執行 TCP/IP 堆疊,完全不接觸 /dev/net/tun。因此,即使主機不會將 TUN 裝置交給容器,這種方式仍可運作。輸入流量仍可正常運作,因為傳入的 tunnel 連線會轉送到 localhost 上的相同連接埠。這就是上述 sidecar 不需要裝置或 capabilities 的原因。

代價主要出現在輸出流量。使用者空間模式下,應用程式無法直接開啟 socket,連線到 tailnet 中的其他節點。tailscaled 會改用 SOCKS5 proxy 和 HTTP proxy,因此要在 tailscale 服務上設定 TS_SOCKS5_SERVER=localhost:1055,並在 app 上設定 ALL_PROXY=socks5://localhost:1055,而且 app 必須支援這些設定。任何忽略 proxy 環境變數的程式都無法連線到 tailnet。

這個堆疊本身也有一些限制。它只承載 TCP 和 UDP,因此 SCTP 等其他 IP protocol 無法通過。ICMP 僅限於 ping,由 daemon 重建,因此會增加些許表面延遲。連線會在節點終止,再重新撥號到目標端,因此不是端對端連線。使用者空間節點也無法使用 exit node,或使用其他人所廣告的 subnet route;但它本身可以廣告這些路由。

需要透明的輸出流量時,請在 tailscale 服務中加入以下 3 項設定,改用 kernel networking。

    environment:
      - TS_USERSPACE=false
    devices:
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin

先使用 test -c /dev/net/tun && echo ok 確認主機能提供該功能。在 KVM 上通常會有此裝置。在共用主機 kernel 的容器虛擬化環境中,該裝置可能不存在,此時只能使用使用者空間模式。如果容器預計要作為 廣告私有網段的 subnet router,或作為 其他裝置使用的 exit node,請將 TUN 裝置提供給容器,因為這些角色在使用者空間模式下受到的限制最大。

連線至服務:直接提供,或使用純 MagicDNS 名稱

最簡單的方式是使用 MagicDNS 名稱。在 tailnet 上的任何裝置中,http://nginx-demo/ 都能運作,完整名稱 http://nginx-demo.your-tailnet.ts.net/ 也可以使用。這裡的純 HTTP 並不是在網路上以明文傳輸,因為 WireGuard 會加密兩個節點之間的流量;協調伺服器能看見與不能看見的內容則界定了這項保護的適用範圍。由於沒有憑證,瀏覽器會將來源標示為不安全,任何要求安全內容環境的網頁功能也會拒絕執行。

另一種方式是使用在容器內執行的 Tailscale Serve。

docker compose exec tailscale tailscale serve --bg localhost:80
docker compose exec tailscale tailscale serve status

這會以 Tailscale 配置的憑證,將應用程式發布在 https://nginx-demo.your-tailnet.ts.net。必須在管理主控台的 DNS 頁面啟用 MagicDNS 和 HTTPS 憑證,否則沒有可寫入憑證的名稱。--bg 會將設定寫入你持久化保存的 tailscaled 狀態,因此容器重新啟動後仍會保留;tailscale serve reset 會移除該設定。若希望將設定保存在 repository 中,而不是寫入 shell 命令,可以讓 TS_SERVE_CONFIG 指向 JSON 檔案。Serve 的服務僅會留在 tailnet 內。Funnel 是另一個命令,會將相同的服務發布到公用網際網路,因此在輸入任一命令前,請先閱讀Serve 與 Funnel 的差異。

失效情況與您會看到的訊息

Error response from daemon: conflicting options: port publishing and the container type network mode。 您在 sidecar 服務上保留了 ports: 區塊。只有擁有該 namespace 的容器可以發布連接埠,而僅限 tailnet 使用的應用程式不應發布任何連接埠。請刪除該區塊。

應用程式容器正在執行,但沒有任何請求到達。 您單獨重新建立了 tailscale 服務。該服務擁有的 namespace 也隨之被銷毀,而應用程式仍連接到已不存在的項目。請使用 docker compose up -d --force-recreate 重新建立這兩個服務。

管理主控台中沒有出現節點。 請查看 docker compose logs tailscale。遭拒的 key 會記錄在其中。已使用過的一次性 key,以及超過到期日的 key,都會在節點加入 tailnet 前使其停止。

節點已啟動,tailscale status 看起來正常,但 curl http://nginx-demo/ 一直等待。 應用程式並未在您預期的位置監聽。請使用 docker compose exec nginx wget -qO- http://localhost/,從共用 namespace 內向應用程式發出請求。如果這裡也失敗,問題出在應用程式,而不是 Tailscale。如果成功,表示應用程式只綁定到一個介面,而不是所有介面。

您停止 stack 一小時後,該機器從主控台消失。 使用的是 ephemeral key。最後一次活動後 30 到 60 分鐘會移除節點,這表示功能正常運作。

從版本 1.78 開始,映像可以公開未經驗證的 /healthz endpoint:請設定 TS_ENABLE_HEALTH_CHECK=true。該 endpoint 會監聽 TS_LOCAL_ADDR_PORT,預設值為 [::]:9002。請將 Compose healthcheck 指向該 endpoint,讓驗證失敗的節點被回報為狀況不良,而不是看似正常地持續運作。如果您不想依賴 Tailscale 的 coordination servers,同一份 compose file 可透過 TS_EXTRA_ARGS=--login-server=https://headscale.example.com 連線到您自己的 control plane,這也是自行執行 Headscale 作為 control server的起點。

FAQ

為什麼 tailnet 上的其他裝置無法連線到我的應用程式容器?

因為 tailscale 容器只讓自己加入 tailnet。如果應用程式在預設的 Compose bridge network 上執行,並使用自己的位址,tailscale node 不會將流量轉送給它;唯一的進入方式就是已發布的主機埠。為應用程式指定 network_mode: service:tailscale,讓它共用 tailscale 容器的 network namespace,然後移除其 ports: 區塊。之後即可透過 tailnet,使用應用程式監聽的埠連線。

在 Docker Compose 中執行 Tailscale 時,需要 /dev/net/tun 嗎?

若只需要接收入站連線,則不需要。TS_USERSPACE 的預設值為 true;在此模式下,tailscaled 會使用自己的 network stack,並將傳入的 tunnel 連線轉送到 localhost 上相同的埠。因此,sidecar 不需要裝置或額外 capabilities 即可運作。當容器必須透明地向 tailnet 建立出站連線,或充當 subnet router 或 exit node 時,才需要 /dev/net/tun、TS_USERSPACE=false 與 net_admin。

Compose stack 應使用 ephemeral 還是可重複使用的 auth key?

對於長期執行的 stack,請使用已加上 tag 且非 ephemeral 的可重複使用 key。加上 tag 會停用 node key expiry,因此容器不會在等待人員重新核准登入的 180 days 後退出 tailnet。只有在經常銷毀 stack 的情況下才選擇 ephemeral,例如 preview environment,因為 Tailscale 會在 ephemeral device 最後一次活動後 30 to 60 minutes 將其移除,讓 admin console 維持整潔。

為什麼我的容器每次重新啟動都會顯示為新機器?

因為 state directory 沒有持久化,所以 tailscaled 每次啟動時都沒有 identity,並以全新的 node 進行驗證。設定 TS_STATE_DIR=/var/lib/tailscale;在 Kubernetes 以外,它沒有預設值,並將該路徑掛載到 bind mount 或 named volume。只設定其中一項,直到第一次 docker compose down 前看起來都正常。請確認掛載的 directory 中存在 tailscaled.state,且 stack 已停止。