用 Tailscale 當 sidecar 容器,家裡的服務不必轉埠、不必 DDNS、不必公開憑證。連帶解決手機瀏覽器不給用剪貼簿與相機的問題。
發布於 2026-09-06・約 5 分鐘
家裡跑了幾個自己用的服務,想在外面用手機開。傳統作法是路由器轉埠、申請 DDNS、跑 Let's Encrypt、再想辦法擋掉全世界的掃描機器人。
我全部沒做。防火牆上沒有開任何一個對外的 port,服務照樣在外面用得到。
services: tailscale: image: tailscale/tailscale:latest container_name: homelab-tailscale hostname: homelab restart: unless-stopped environment: - TS_AUTHKEY=${TS_AUTHKEY} - TS_STATE_DIR=/var/lib/tailscale - TS_USERSPACE=false volumes: - tailscale_state:/var/lib/tailscale devices: - /dev/net/tun cap_add: - net_admin caddy: image: caddy:2-alpine network_mode: service:tailscale # 共用 tailscale 的網路 stock: build: ./services/stock network_mode: service:tailscale
network_mode: service:tailscale 讓這些容器不要有自己的網路,直接加進 Tailscale 容器的網路命名空間。
network_mode: service:tailscale
結果是:整組服務在 tailnet 上只是一個節點,叫 homelab。手機裝了 Tailscale 之後,直接開 http://homelab/ 就進來了,不管人在哪裡。彼此之間用 127.0.0.1:8001 互連,因為它們字面上就在同一個 loopback 上。
homelab
http://homelab/
127.0.0.1:8001
TS_STATE_DIR 一定要掛 volume。沒掛的話容器每次重建都要重新認證,而 auth key 通常是一次性的。
TS_STATE_DIR
TS_USERSPACE=false 走核心模式,效能比較好,前提是拿得到 /dev/net/tun。拿不到就設 true 退回使用者空間模式,會慢一點但能動。
TS_USERSPACE=false
/dev/net/tun
true
我的反向代理上沒有 basic auth,服務也沒有登入頁。
因為只有 tailnet 上的裝置連得到這個位址,而 tailnet 上只有我自己的機器。認證在網路層已經做完了,再做一次是重複的。
這個決定有明確的代價,要講清楚:手機掉了,撿到的人打開瀏覽器就看得到所有服務。 我接受這個代價,因為手機本身有鎖,而且我可以在 Tailscale 後台把那台裝置踢掉。你如果不接受,就在反向代理那層補一道 basic auth,設定改三行的事。
不能接受的是「反正在內網」然後什麼都不想。內網不是安全邊界,它只是把攻擊面從全世界縮小到「拿得到你其中一台裝置的人」。
純 HTTP 的 http://homelab/ 能動,我一開始就用這個。後來卡住了:手機上有個「一鍵貼上」的功能讀不到剪貼簿。
原因是瀏覽器只在安全內容(https 或 localhost)底下才開放剪貼簿、相機、通知、地理位置這些 API。http://homelab/ 兩個都不是,所以那些 API 直接不存在,不是回錯誤,是 navigator.clipboard 根本 undefined。
navigator.clipboard
Tailscale 可以幫你簽憑證並終結 TLS。開法是給容器一份 serve 設定:
{ "TCP": { "443": { "HTTPS": true } }, "Web": { "${TS_CERT_DOMAIN}:443": { "Handlers": { "/": { "Proxy": "http://127.0.0.1:80" } } } } }
然後掛給容器:
environment: - TS_SERVE_CONFIG=/config/serve.json
${TS_CERT_DOMAIN} 由容器啟動時代入你這台的 tailnet 網域,不用自己填。
${TS_CERT_DOMAIN}
前提是要先去 Tailscale 管理後台開 HTTPS Certificates(admin console → DNS → Enable HTTPS)。沒開的話簽不到憑證,而且錯誤訊息不太明顯。
開完之後 https://homelab.<你的-tailnet>.ts.net/ 就通了,純 HTTP 的入口不受影響,兩個並存。
https://homelab.<你的-tailnet>.ts.net/
一、Port 是共用的。 所有服務在同一個 port 空間,不能撞號。加服務前要先確認號碼沒被佔走。
二、用了 network_mode 就不能再宣告 ports 或 networks。 這兩個欄位跟它互斥,要對外開 port 只能寫在 Tailscale 那個容器上。
network_mode
ports
networks
三、重建容器要整組一起重建。 這是最容易吃虧的一條。Tailscale 容器一被重建就是一個新的網路命名空間,其他沒被一起重建的容器會留在舊的、已經不存在的命名空間上。docker ps 照樣顯示 Up,網路卻是死的,而且沒有任何錯誤訊息。
docker ps
Up
所以動到網路相關的東西時,不要點名服務:
docker compose up -d --no-build
之後驗一下每個容器實際綁在哪:
TS=$(docker inspect homelab-tailscale --format '{{.Id}}') for c in $(docker ps --format '{{.Names}}'); do m=$(docker inspect "$c" --format '{{.HostConfig.NetworkMode}}') case "$m" in container:*) [ "$m" = "container:$TS" ] && echo "OK $c" || echo "STALE $c" ;; esac done
不用轉埠、不用 DDNS、不用管憑證續期、不用擋掃描流量、不用在每個服務各設一套登入。搬機器的時候 compose 檔直接跟著走,網路那段完全不用改。
換來的是「所有服務綁在一個網路命名空間上」這個耦合,以及上面那三個代價。對一台自己用的機器來說我覺得很划算;如果是要給不特定人用的服務,這條路從一開始就不適用——Tailscale 的前提是「連進來的人已經是你信任的裝置」。
標籤:Tailscale、Docker、自架服務、內網、HTTPS