Headscale 自托管 Tailscale:VPS 安装与首个节点接入
在 Ubuntu 24.04 VPS 上用官方 .deb 安装 Headscale,先设置 server_url 再启动服务,并将首个 Linux、macOS 或 Windows 节点加入自建 tailnet。
Headscale 是什么
Headscale 是 Tailscale 控制服务器的自托管实现。因此,协调私有网络的计算机是您拥有的 VPS。它是一个社区项目,不由 Tailscale Inc. 运营。每台计算机仍运行官方 tailscale 客户端,只需使用一个标志将其指向您的服务器:--login-server。
控制服务器负责记录哪些设备属于该网络。它会从 100.64.0.0/10 中为每个节点分配地址,分发公钥,并告知节点如何发现彼此。隧道仍使用 WireGuard,在节点之间直接建立。除非无法建立直连路径、节点退回到中继,否则您两台计算机之间的流量不会经过 headscale 服务器。自行运行这一协调角色,改变的是由谁持有它,而不是它能够执行的功能。因此,在您将这次迁移本身视为安全性提升之前,值得先了解控制服务器在此模型中能够和不能访问什么。
每个 Headscale 实例服务一个 tailnet(一个 Tailscale 网络)。该项目认为这种模式适合个人使用或小型组织。如果只有三四台计算机,在您拥有的 VPS 上运行普通 WireGuard VPN,需要维护的软件更少,出错点也更少。当您不再希望为每台新笔记本手动编写一个 [Peer] 块时,Headscale 才更有价值。成本通常是人们最初开始了解它的原因,因此在承担服务器成本之前,值得先阅读托管免费方案实际包含哪些内容,因为少量个人设备通常可以包含在其中。如果您已经超过该上限,请将成本与付费方案的价格进行比较;其计费单位是用户而不是设备,因为同一账户下的家庭用户即使设备数量增加很久,费用也可能仍然较低。如果您希望使用自托管控制平面,但更想拥有自己的客户端和用于管理对等节点的 Web 界面,而不是 Tailscale 的直接替代品,那么在单个 VPS 上运行 NetBird值得考虑。有关这两种模式的更广泛比较,请参阅WireGuard 与 Tailscale 的区别。
安装前的准备条件
- 一台运行 Ubuntu 24.04 的 VPS,具有公网 IPv4 地址和 sudo 访问权限。如果服务器是新建的,请先完成新 VPS 的前十分钟。
- 一个指向该地址的 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 systemd 用户,写入默认的 /etc/headscale/config.yaml,并安装 systemd 单元。它不会启动服务,这正是正确的顺序。随软件包提供的配置会将 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 应用相同的三项修改。保留原始文件副本,因为该文件很长且包含大量注释,是后续配置其他设置时最好的参考。
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 是进程绑定的地址。将其保留为回环地址。服务器上的反向代理负责终止 TLS(传输层安全)并将请求转发到该地址,因此服务器外部无需访问 8080 端口。
base_domain 是 MagicDNS 后缀,即节点获取名称时使用的域名。它必须是不带末尾句点的完全限定域名,并且必须与 server_url 中的域名不同,否则两个名称空间会发生冲突。
不要修改数据库部分。默认数据库是位于 /var/lib/headscale/db.sqlite 的 SQLite,软件包已创建并拥有该目录。对于此规模的 tailnet,SQLite 已经足够。
启动 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 读取日志。此阶段的故障几乎总是由配置文件导致,因为 headscale 会在打开套接字前解析整个文件。因此,缩进错误或未知键会在进程开始监听前使其退出。修正文件后,执行 sudo systemctl restart headscale。之后每次修改配置都需要执行同样的重启操作。客户端随后会自动重新连接。如果您刚开始接触 systemd 单元,使用 systemd 运行自有服务和定时器介绍了这里使用的命令。
在 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 $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,以及将请求重定向到 HTTPS;Caddy 要获取证书,也必须使用该端口。
端口 8080 保持关闭。listen_addr 是 127.0.0.1:8080,因此代理通过回环接口访问 headscale,不涉及任何防火墙规则。向互联网开放 8080 会为客户端提供明文控制通道,但没有任何实际收益。请注意,大多数服务商还会在控制面板中运行独立于 UFW 的第二层防火墙。因此,某个端口可能已在服务器上开放,但在边界处仍处于关闭状态。VPS 上的 UFW 防火墙基础将更详细地介绍规则语法。
创建用户和预认证密钥
sudo headscale users create alice
sudo headscale users listheadscale 命令是客户端。它通过 Unix 套接字 /var/run/headscale/headscale.sock 与正在运行的守护进程通信。该套接字的模式为 0770,所属组为 headscale。这会产生两个影响。服务停止时,该命令会失败;这也是本指南需要按此顺序操作的另一个原因。除非将您自己的账户加入 headscale 组,否则运行该命令需要 sudo。
users list 会在每个名称旁打印一个 ID。您需要记录这个数字,因为密钥命令接受数字形式的用户 ID,而不是名称。
sudo headscale preauthkeys create --user 1 --expiration 24h密钥只会显示一次。请立即复制。预认证密钥只能使用一次,默认有效期为 one hour,除非您另行指定。因此,在仍处于测试阶段时,设置 --expiration 24h 很有必要。添加 --reusable 可创建一个用于注册多台计算机的密钥。请像保护密码一样保护该密钥,因为持有它的任何人都可以加入您的网络。
使用 --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 完全一致,包括 scheme,且末尾不能有斜杠。系统会按字符串进行比较。如果不一致,客户端会先向一个地址注册,随后又被要求与另一个地址通信。
之前登录过 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 命令批准已发布的路由,而不是在托管管理控制台中批准。如果您需要的是访问该 VPS 后方的专用网络,而不是通过它访问互联网,那么同样的批准步骤也适用于向 tailnet 的其他节点发布该子网。从某个节点发布一个应用,而不是通过该节点路由整个网络,是另一项工作;serve 和 funnel 是实现这一目标的两种方式。不过,两者都依赖 Tailscale 自有的证书和入口机制,因此应将其视为托管 tailnet 功能,而不是 headscale 提供的功能。
DERP,以及直连失败时负责中继流量的组件
DERP(数据包专用加密中继)是备用路径。当两个节点无法建立直接的 WireGuard 连接时,通常是因为它们都位于严格的 NAT(网络地址转换)之后,此时会改为通过中继发送数据包。中继不会持有任何密钥,因此无法读取您的流量。但它可以看到哪些节点正在通信,以及传输了多少数据。
请明确默认配置的行为。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(用于 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 密钥会按设计快速过期。默认有效期为 one hour,且只能使用一次。如果 tailscale up 拒绝该密钥,请在服务器上生成新密钥,不要在客户端编辑任何内容。
节点密钥是长期有效的部分。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 开始,升级路径受到严格限制。不能跳过次要版本,也不能降级到更早的次要版本。每次只升级一个次要版本,每一步之前都先备份,并先阅读该版本的发行说明,因为同一版本更改了 ACL 策略行为,并移动了多个配置项。
FAQ
为什么安装 .deb 后 headscale 无法立即启动?
软件包会安装服务单元,但不会启动服务。默认的 /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 客户端吗?
需要。Headscale 只替代控制服务器。每个节点都运行 Tailscale 官方客户端,并通过 sudo tailscale up --login-server https://headscale.example.com 指定您的服务器。标准客户端已支持此标志,因此无需修改或重新构建客户端。
我的流量会经过 headscale 服务器吗?
通常不会。Headscale 负责协调网络并分发密钥和地址,数据路径则由节点之间的 WireGuard 直接建立。只有当两个节点无法直接连通、改用 DERP 中继时,流量才会绕行。使用随软件包提供的配置时,这些中继是 Tailscale 的公共中继。在节点上运行 tailscale status,即可查看指定对端是 direct,还是使用 relay。
为什么节点注册后仍然处于离线状态?
如果节点出现在 headscale nodes list 中却始终无法上线,通常是反向代理丢失了控制连接。该连接是一个带有 Upgrade: tailscale-control-protocol 标头的 POST HTTP 升级请求。除非添加 map $http_upgrade $connection_upgrade 块和对应的 proxy_set_header 行,否则 nginx 会丢弃该请求。Caddy 无需额外配置即可转发此连接,因此可以快速用于判断问题是否出在代理上。
headscale 是否需要域名和 TLS?
实际使用中需要。客户端会连接您在 server_url 中填写的字符串。证书会签发给域名,而不是裸 IP 地址;配置文件也说明 DERP 需要 TLS。配置域名并使用 Caddy 大约需要五分钟,即可获得会自动续期的 HTTPS 端点。通过纯 HTTP 运行控制服务器,意味着每个客户端与服务器之间的通信都会以明文经过互联网。