Headscale 自架 Tailscale 控制伺服器教學
在 VPS 上自架 Tailscale control server,從官方 .deb 安裝 headscale,先設定 server_url 再啟動服務,最後加入第一台節點。
headscale 是什麼
Headscale 是 Tailscale control server 的 self-hosted 實作,因此負責協調私有網路的機器會是你擁有的 VPS。這是社群專案,不是由 Tailscale Inc. 執行。每台機器仍會執行官方的 tailscale client,只要加上一個 flag --login-server,即可指定使用你的 server。
Control server 負責識別哪些節點屬於這個網路。它會從 100.64.0.0/10 配置位址給每個節點、分發 public key,並告知各節點彼此的位置。通道仍使用 WireGuard,由節點直接建立。除非無法建立直接路徑,導致節點改用 relay,否則兩台機器之間的 traffic 不會經過 headscale 主機。
每個 headscale instance 只提供一個 tailnet(即一個 Tailscale network)。專案將此定位為適合個人或小型組織使用。若只有三或四台機器,在你擁有的 VPS 上使用一般 WireGuard VPN 需要執行的軟體較少,也較少發生故障。當你不想再為每台新 laptop 手動撰寫 [Peer] 區塊時,Headscale 才能發揮效益。如果你想使用 self-hosted control plane,但比較希望採用自己的 client,以及用於管理 peer 的 web interface,而不是 Tailscale 的 drop-in replacement,在單一 VPS 上使用 NetBird 是值得評估的替代方案。如需比較這兩種模型的詳細內容,請參閱WireGuard 與 Tailscale 的差異。
安裝前的準備事項
- 一台執行 Ubuntu 24.04 且具備公開 IPv4 位址及 sudo 存取權限的 VPS。若伺服器是新建的,請先完成新 VPS 的前 10 分鐘。
- 一筆指向該位址的 DNS A 記錄。本指南使用
headscale.example.com。 - 供 MagicDNS 使用的第二個網域或子網域。本指南使用
tailnet.example.net。它不得與server_url中的網域相同。 - 一台要加入的用戶端機器,作業系統可為 Linux、macOS、Windows、Android 或 iOS。
從官方 .deb 安裝 headscale
此專案會在 GitHub releases 頁面發布 .deb 套件。截至 2026 年 7 月,目前版本為 0.29.3。請先確認架構,因為檔名會包含架構資訊。
sudo apt update
sudo apt install -y wget
dpkg --print-architecture在一般 x86 VPS 上,此命令會輸出 amd64;在 Ampere 或 Graviton 類型的方案上,則會輸出 arm64。請將結果放入下方變數。
HEADSCALE_VERSION="0.29.3"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb \\
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install -y ./headscale.deb
headscale version檔名前方必須加上 ./。如果省略此項,apt 會在套件庫中尋找名為 headscale.deb 的套件,並執行失敗。
此套件會建立 headscale system user、寫入預設的 /etc/headscale/config.yaml,並安裝 systemd unit。它不會啟動服務,這樣的順序才正確。套件附帶的設定會將 server_url 指向 http://127.0.0.1:8080。這不是你的任何用戶端都能連線的位址,因此此時啟動服務會得到錯誤設定,即使服務能夠啟動也一樣。在此階段執行 sudo systemctl is-active headscale 會輸出 inactive。這是預期結果,不是錯誤。
在啟動服務前設定 server_url
使用 sudo nano /etc/headscale/config.yaml 編輯 /etc/headscale/config.yaml,或使用 sed 套用相同的 3 項變更。保留原始檔案副本,因為該檔案內容很長且包含大量註解,是後續設定最好的參考資料。
sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.orig
sudo sed -i 's|^server_url:.*|server_url: https://headscale.example.com|' /etc/headscale/config.yaml
sudo sed -i 's|^listen_addr:.*|listen_addr: 127.0.0.1:8080|' /etc/headscale/config.yaml
sudo sed -i 's|^ base_domain:.*| base_domain: tailnet.example.net|' /etc/headscale/config.yaml
sudo grep -E '^(server_url|listen_addr):|^ base_domain:' /etc/headscale/config.yamlserver_url 是 headscale 寫入每個用戶端註冊資訊的位址。之後用戶端會持續連線到這個完全相同的字串,因此必須是前面加上 https:// 的公開名稱,不能使用 127.0.0.1。
listen_addr 是程序繫結的位址。請維持在 loopback。相同主機上的反向代理會終止 TLS(transport layer security),再將流量轉送到該位址,因此伺服器外部不需要連線到 port 8080。
base_domain 是 MagicDNS suffix,也就是節點取得名稱所使用的網域。它必須是完整網域名稱,且結尾不可有句點;同時必須與 server_url 中的網域不同,否則兩個名稱空間會發生衝突。
請勿修改資料庫區段。預設值是位於 /var/lib/headscale/db.sqlite 的 SQLite。該目錄由套件建立並擁有,而 SQLite 足以支援這個規模的 tailnet。
啟動 headscale 並確認服務正在執行
sudo systemctl enable --now headscale
sudo systemctl is-active headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/healthis-active 會輸出 active,而 curl 會輸出 200。enable --now 會完成這兩項工作:啟動服務,並設定服務在重新開機後自動啟動。
如果 is-active 輸出 failed,請使用 sudo journalctl -u headscale -n 50 --no-pager 讀取 journal。此階段的失敗原因幾乎總是設定檔,因為 headscale 會先剖析完整檔案,再開啟 socket。因此,縮排錯誤或未知金鑰會在程序開始監聽前就使其停止。修正檔案後,再執行 sudo systemctl restart headscale。之後每次修改設定都需要執行相同的 restart。用戶端會自行重新連線。如果你不熟悉 systemd unit,請參閱使用 systemd 執行自有服務與 timer,其中涵蓋本節使用的命令。
在 shell 中檢查狀態檔案:
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.key兩行都以 headscale 開頭,這是套件建立的非特權使用者。noise_private.key 是伺服器向用戶端識別自身的身分。請保留此檔案。若刪除該檔案,headscale 會產生新的身分,所有節點都必須重新註冊。
在 headscale 前方加入 TLS
用戶端必須透過 HTTPS 連線至 server_url。Caddy 是最簡單的方式,因為它會自行申請及續期憑證。
sudo apt install -y caddy將 /etc/caddy/Caddyfile 替換為 headscale 文件中的區塊:
headscale.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl is-active caddy檔案剖析成功時,validate 會輸出 adapted config to JSON。檔案未格式化的警告不影響功能。從筆記型電腦執行 curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health 時,也應輸出 200。這項單一檢查即可證明 DNS、防火牆、憑證及代理伺服器已協同運作。
以下是最容易讓人花上一晚排查的代理伺服器細節。Tailscale 控制連線使用 HTTP upgrade,透過 POST 而非 GET 建立,且 Upgrade 標頭的值為 tailscale-control-protocol。Caddy 不需額外設定即可轉送這些內容。nginx 則不會,因此 nginx 前端需要加入 upgrade map:
map $http_upgrade $connection_upgrade {
default keep-alive;
'' close;
}
server {
listen 443 ssl;
server_name headscale.example.com;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_pass http://127.0.0.1:8080;
}
}如果省略這些行,一般請求仍會成功。因此 /health 會回傳 200,表面上看起來一切正常,但長時間維持的控制連線永遠無法建立,節點註冊後便保持離線。如果選擇 nginx,在 Ubuntu 24.04 搭配 nginx 使用 Certbot 說明憑證設定部分。
UFW 應開放哪些連接埠
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose連接埠 443 承載所有用戶端連線。連接埠 80 僅用於 ACME(自動憑證管理環境)的 HTTP challenge,以及將 HTTP 重新導向至 HTTPS;Caddy 也必須使用它才能取得憑證。
連接埠 8080 維持關閉。listen_addr 是 127.0.0.1:8080,因此 proxy 會透過 loopback interface 連線至 headscale,不涉及任何防火牆規則。對 internet 開放 8080 會讓用戶端取得未加密的控制通道,沒有任何實際效益。請注意,多數 provider 會在控制面板中另外執行一層 firewall,與 UFW 分開管理。因此,某個連接埠可能已在主機上開放,但在網路邊界仍然關閉。VPS 上的 UFW firewall 基礎會更詳細說明規則語法。
建立使用者與 preauth key
sudo headscale users create alice
sudo headscale users listheadscale 指令是用戶端。它會透過位於 /var/run/headscale/headscale.sock 的 Unix socket,與正在執行的 daemon 通訊。該 socket 的模式為 0770,且由 headscale 群組擁有。這會帶來兩個結果。服務停止時,指令會失敗;這也是本指南必須依照順序操作的另一個原因。此外,除非將自己的帳號加入 headscale 群組,否則執行時需要 sudo。
users list 會在每個名稱旁列出 ID。你需要這個數字,因為 key 指令接受的是數字 user ID,而不是名稱。
sudo headscale preauthkeys create --user 1 --expiration 24hkey 只會顯示一次。請立即複製。preauth key 僅能使用一次,除非另行指定,否則有效期限為 1 小時。因此,在仍處於測試階段時,值得設定 --expiration 24h。加入 --reusable 可建立能讓多台機器註冊的 key;請將該 key 視同密碼保管,因為持有它的任何人都能加入你的網路。
使用 --login-server 連線第一個用戶端
在要加入的機器上執行:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com --auth-key 'hskey-auth-PASTE-YOUR-KEY-HERE'
tailscale status
tailscale ip -4tailscale ip -4 會輸出 headscale 指派的位址,例如 100.64.0.1。返回伺服器後,sudo headscale nodes list 會顯示該節點的 ID、使用者及上線狀態。
--login-server 的值必須與 server_url 完全相同,包括配置方式,且結尾不得有斜線。系統會以字串比較兩者;若不一致,用戶端會先向其中一個位址註冊,之後卻被要求連線到另一個位址。
先前登入過 Tailscale 代管服務的機器會保留該登入狀態。先在該機器上執行 sudo tailscale logout,再搭配 --login-server 執行 tailscale up。
如果省略 --auth-key,用戶端會改為輸出 URL。開啟該 URL 後,頁面會顯示這次註冊嘗試的識別碼。接著在伺服器上核准:
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGE這種表單較適合自己的筆記型電腦。對於需要以腳本執行的工作,預授權金鑰更合適,因為不需要人工持續查看。VPS 本身成為節點後,也能承載其他機器的網際網路流量;這就是出口節點設定。唯一不同之處是,您必須在伺服器上使用 headscale 命令核准所公告的路由,而不是在代管服務的管理主控台中核准。
DERP,以及直接路徑失效時轉送網路流量的機制
DERP(designated encrypted relay for packets,指定封包加密中繼)是備援路徑。兩個節點無法建立直接的 WireGuard 連線時,通常是因為兩者都位於嚴格的 NAT(network address translation,網路位址轉譯)之後,這時會改由中繼轉送封包。中繼不持有任何金鑰,因此無法讀取流量內容。但它能看見哪些節點正在通訊,以及傳輸了多少資料。
請先了解預設設定的行為。Headscale 預設指向 https://controlplane.tailscale.com/derpmap/default,並使用 auto_update_enabled: true 與 update_frequency: 3h,因此控制平面由你管理,但中繼由 Tailscale 提供。對多數人而言,這是合理的取捨。如果你不接受這種安排,請自行執行中繼。
若要執行自有中繼,請在 config.yaml 的 derp.server 下設定 enabled: true,重新啟動 headscale,並使用 sudo ufw allow 3478/udp 開放 STUN(session traversal utilities for NAT,NAT 工作階段穿透工具)連接埠。設定檔已明確說明此要求:server_url 必須使用 https,因為 DERP 需要 TLS。清空 derp.urls 清單會將 Tailscale 的中繼從對應表移除。如果在沒有可運作的內嵌中繼時這樣做,任何無法直接連線的節點組合都將完全無法連線。
從用戶端執行 tailscale netcheck,會列出它所知各個中繼區域的延遲;tailscale status 則會將每個對等端標記為 direct(附有位址)或 relay(附有區域代碼)。對等端卡在 relay 時,問題出在 NAT,而不是 headscale。若對等端為 direct 但速度仍然很慢,則是另一個問題;通常應先檢查 MTU,而不是隧道本身。
為什麼節點顯示為離線?
代理伺服器捨棄了升級請求。 這是最常見的原因。其特徵是其他項目都正常:/health 回傳 200、headscale nodes list 顯示該節點,但節點始終無法上線。控制連線是承載 Upgrade: tailscale-control-protocol 的 POST 請求。未轉送此請求的代理伺服器會中斷唯一能回報節點狀態的通道。請將 nginx 設定與上方的 map 區塊比對,或改用 Caddy 以排除代理伺服器問題。
server_url 在節點註冊後變更。 節點會持續撥號連線至註冊時取得的值。若您修改了該值,請在每個節點上執行 sudo tailscale up --login-server https://headscale.example.com --force-reauth。
用戶端未執行。 在節點上執行 sudo systemctl is-active tailscaled 和 sudo journalctl -u tailscaled -n 50 --no-pager。無法解析或連線至您網域的用戶端,會在這些位置記錄重試訊息。
金鑰已過期。 下一節將說明此問題。
測試時,請在 VPS 上執行 sudo journalctl -u headscale -f 以監看伺服器端,並在用戶端重新啟動 tailscaled。能連線至 headscale 的節點會立即產生日誌行。若沒有任何訊息,表示請求尚未抵達,因此應先檢查 DNS、防火牆與代理伺服器,再檢查 headscale。
金鑰到期,以及幾週後停止運作的節點
這裡有兩種不同的到期機制。混淆兩者會浪費排查時間。
Preauth key 會依設計快速到期。預設期限為一小時,且只能使用一次。如果 tailscale up 拒絕該金鑰,請在伺服器上產生新的金鑰,不要修改用戶端上的任何內容。
Node key 的有效期較長。config.yaml 的 node 區段會設定 expiry: 0,而 0 表示沒有預設到期時間:已註冊的節點會持續有效,直到你使其到期為止。標記節點則永遠不會到期。若希望註冊項目經過一段時間後自動失效,請設定 expiry: 180d,但必須了解這代表什麼:之後每個未標記節點都必須依該排程執行 sudo tailscale up --login-server https://headscale.example.com --force-reauth,而無人重新驗證的無頭伺服器會自行從網路中脫離。
當有人遺失筆電時,請手動處理。sudo headscale nodes list 會提供該節點的 ID,接著 sudo headscale nodes expire -i 3 會將該節點登出,sudo headscale nodes delete -i 3 則會將它從網路中完全移除。
備份與升級
/var/lib/headscale 與 /etc/headscale 合在一起就是整台伺服器的完整狀態。複製前先停止服務,因為 SQLite 可能仍有寫入作業,而在負載下複製資料庫可能導致資料不一致。
sudo systemctl stop headscale
sudo tar czf /root/headscale-state.tgz -C /var/lib headscale
sudo tar czf /root/headscale-config.tgz -C /etc headscale
sudo systemctl start headscale
sudo chmod 600 /root/headscale-*.tgz將這兩個檔案移出該伺服器。檔案內含私密金鑰與所有註冊資料,因此應與伺服器本身同等謹慎地保護。VPS 的 restic 備份 說明如何排程執行並加密備份。
升級流程與安裝相同:下載新版 .deb、sudo apt install ./headscale.deb,然後重新啟動,再重新執行 is-active 與 /health 檢查。從 0.29 開始,升級路徑受到嚴格限制。不得跳過次要版本,也不得降級至較舊的次要版本。每次只升級一個次要版本,並在每個步驟前建立備份。先閱讀該版本的 release notes,因為同一個 release 變更了 ACL policy 的行為,並移動了數個設定金鑰。
FAQ
為什麼安裝 .deb 後,headscale 無法立即啟動?
套件會安裝 unit,但服務會維持停止狀態,而且預設的 /etc/headscale/config.yaml 只是範本,不是可直接使用的設定。先編輯 server_url、listen_addr 和 base_domain,再執行 sudo systemctl enable --now headscale,並以 sudo systemctl is-active headscale 確認。如果仍然失敗,sudo journalctl -u headscale -n 50 --no-pager 會指出問題所在。此時幾乎都是 YAML 錯誤,因為 headscale 會先解析整個檔案,之後才繫結埠。
我還需要在各台機器上安裝一般的 Tailscale client 嗎?
需要。Headscale 只取代 control server。每個 node 都執行 Tailscale 提供的官方 client,並使用 sudo tailscale up --login-server https://headscale.example.com 指向您的 server。標準 client 已支援這個 flag,因此不需要修補或重新建置。
我的 network traffic 會經過 headscale server 嗎?
通常不會。Headscale 負責協調 network,並分配 keys 和 addresses;data path 則是各 node 之間直接使用 WireGuard。只有在兩個 node 無法直接互相連線,改用 DERP relay 時,traffic 才會繞行。依 shipped configuration,這些 relay 是 Tailscale 的公開 relay。請在 node 上執行 tailscale status,查看指定 peer 是 direct 還是位於 relay。
為什麼我的 node 註冊後仍維持 offline?
如果 node 出現在 headscale nodes list 中,卻始終沒有 online,通常表示它在 reverse proxy 的 control connection 已中斷。這個 connection 是以 POST 傳送、並帶有 Upgrade: tailscale-control-protocol header 的 HTTP upgrade;除非加入 map $http_upgrade $connection_upgrade block 和相符的 proxy_set_header lines,否則 nginx 會將其丟棄。Caddy 不需要額外設定即可轉送,因此可快速用來確認問題是否出在 proxy。
headscale 需要 domain name 和 TLS 嗎?
實務上需要。Clients 會連線到您填入 server_url 的字串;certificates 是核發給名稱,而不是裸 IP address;設定檔也明確指出 DERP 需要 TLS。搭配 Caddy 使用 domain 約需五分鐘,即可取得會自動續期的 HTTPS endpoint。若讓 control server 透過明文 HTTP 執行,所有 client 與其之間的通訊都會以明文穿越 internet。