Headscale自部署指南:用Docker搭建Tailscale开源替代方案
痛点:免费的Tailscale只有3个用户
用 Tailscale 组网非常爽——装个客户端,登录账号,所有设备就在一个虚拟局域网里了。问题是:免费版最多 3 个用户。
当你的设备超过 3 台——笔记本、台式机、两台服务器、手机——免费版直接不够用。付费版 $6/月起,一年下来也小几百块。而且数据经过 Tailscale 的中继服务器,虽然加密了,但总有那么点不放心。
Headscale 就是答案。
Headscale 是 Tailscale 控制服务器的开源实现,完全兼容 Tailscale 客户端。你只需要一台有公网 IP 的 VPS,就能自建一套功能完整的 Tailscale 组网服务。没有用户数限制,数据不经第三方,连 DERP 中继服务器也能自建。
本文用 Docker Compose 完成全套部署:Headscale 控制服务 + Caddy HTTPS 反向代理 + headscale-ui 管理面板 + 自建 DERP 中继。
Headscale ≠ VPN:理解 P2P Mesh 组网
在动手部署之前,先搞清楚 Headscale 在整个组网中扮演的角色。
Tailscale/Headscale 不是传统的 VPN(Virtual Private Network),而是一种 P2P Mesh 网络。两者的核心区别:
| 特性 | 传统 VPN(OpenVPN/WireGuard) | P2P Mesh(Tailscale/Headscale) |
|---|---|---|
| 拓扑 | 中心辐射型(Hub-and-Spoke) | 全互联型(Full Mesh) |
| 流量路径 | 全部经过 VPN 服务器 | 直连(P2P),不通则走中继 |
| 带宽瓶颈 | 服务器带宽 | 各节点带宽 |
| 单点故障 | 服务器挂了网络全挂 | 控制服务仅处理信令,挂了不影响已有连接 |
| 配置复杂度 | 每台设备要写配置文件 | 零配置,登录即用 |
| 适用场景 | 公司远程接入内网 | 个人/团队设备互联 |
Headscale 的工作机制用一个简单的流程就能说明:
设备A ──注册到──▶ Headscale 控制服务器设备B ──注册到──▶ Headscale 控制服务器
控制服务器告诉 A:"B 的公网 IP 是 x.x.x.x:3478"控制服务器告诉 B:"A 的公网 IP 是 y.y.y.y:3478"
设备A ◀───直接 UDP 连接───▶ 设备B ↕ NAT 穿透失败时走 DERP 中继关键点:Headscale 只负责控制面——处理注册、分配 IP、交换节点信息。真正的数据传输是设备之间的直连 UDP(STUN 打洞),流量完全不经过 Headscale 服务器。只有 NAT 穿透失败时,才回退到 DERP 中继。
部署架构
我们用 Docker Compose 部署三个服务:
┌─────────────────────────────────────┐│ VPS(公网) ││ ││ ┌──────────┐ ┌──────────┐ ││ │ Headscale │ │ Caddy │ ││ │ :8080 │◀─│ HTTPS │────8080││ │ :9090 │ │ :443 │ ││ └─────┬────┘ └──────────┘ ││ │ ││ ┌─────┴────┐ ││ │ headscale │ ││ │ -ui │ ││ └──────────┘ ││ ││ 端口:443(HTTPS) 3478(STUN/DTLS) │└─────────────────────────────────────┘环境要求
- 一台有公网 IP 的 VPS(1 核 1GB 内存足够)
- 一个域名(例如
hs.example.com),解析到 VPS 的 IP - 安装 Docker 和 Docker Compose
Docker Compose 部署
第一步:创建目录结构
mkdir -p /opt/headscale/{config,data,ui} && cd /opt/headscale第二步:编写 Docker Compose 配置
新建 docker-compose.yml:
version: "3.9"
services: headscale: image: headscale/headscale:0.29.1 container_name: headscale restart: unless-stopped volumes: - ./config:/etc/headscale - ./data:/var/lib/headscale ports: - "3478:3478/udp" # STUN/DERP over DTLS command: serve networks: - headnet
headscale-ui: image: ghcr.io/gurucomputing/headscale-ui:latest container_name: headscale-ui restart: unless-stopped networks: - headnet
caddy: image: caddy:2-alpine container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - ./data/caddy:/data networks: - headnet
networks: headnet: driver: bridge注意:Headscale 官方并不正式支持 Docker 部署(文档建议直接二进制运行),但社区实践证明 Docker Compose 方案稳定可靠。
第三步:生成 Headscale 配置
docker run --rm headscale/headscale:0.29.1 headscale config > config/config.yaml编辑 config/config.yaml,修改关键参数:
# 你的域名server_url: https://hs.example.com:443# Headscale 内部监听地址listen_addr: 0.0.0.0:8080# 内部 gRPC(UI 使用)metrics_listen_addr: 0.0.0.0:9090# IP 分配范围ip_prefixes: - 100.64.0.0/10# DERP 配置derp: server: enabled: true region_id: 999 region_code: "selfhost" region_name: "Self-hosted DERP" stun_listen_addr: "0.0.0.0:3478" private_key_path: /var/lib/headscale/derp_server_key urls: [] auto_update_enabled: false# 随机化节点端口(提高 STUN 打洞成功率)randomize_client_port: true第四步:编写 Caddyfile
hs.example.com { reverse_proxy headscale:8080}
ui.hs.example.com { reverse_proxy headscale-ui:80}第五步:启动
docker compose up -d检查状态:
docker compose psdocker compose logs -f headscale看到 Started headscale 日志就说明启动成功了。
创建用户并接入客户端
创建用户
# 在 headscale 容器中执行docker compose exec headscale headscale users create myhome
# 创建预授权密钥(有效期24小时)docker compose exec headscale headscale preauthkeys create --user myhome --expiration 24hLinux/macOS 客户端接入
在客户端设备上安装 Tailscale(注意:不是 Headscale 客户端,Tailscale 官方客户端完全兼容 Headscale):
# 安装 Tailscalecurl -fsSL https://tailscale.com/install.sh | sh
# 连接到自建 Headscalesudo tailscale up --login-server=https://hs.example.com --authkey=<上一步生成的密钥>浏览器打开返回的链接,登录后设备就加入网络了。
Windows 客户端
- 从 Tailscale 官网 下载 Windows 客户端
- 按住 Shift 右键点击系统托盘中的 Tailscale → Run
tailscale up - 在弹出的终端中输入:
tailscale up --login-server=https://hs.example.com --authkey=<密钥>验证连接
在任何一台加入网络的设备上:
tailscale status你应该能看到所有已连接的设备:
100.64.0.1 headscale john@ linux -100.64.0.2 laptop john@ macOS active; direct x.x.x.x:3478100.64.0.3 server-prod john@ linux active; relay "selfhost"direct 表示是直连(P2P 打洞成功),relay 表示走 DERP 中继。
DERP 中继部署与配置
DERP(Detoured Encrypted Relay Protocol)是 Tailscale/Headscale 的保底机制——当两个设备之间的 NAT 穿透失败时(比如双方都在对称 NAT 后面),流量会通过 DERP 中继服务器转发。
自建 DERP 服务器
如果上一步已经有设备显示 relay 状态,说明你已经在用自带的 DERP 了(因为配置中开启了 derp.server.enabled: true)。但实际上这个 DERP 和 Headscale 进程共用同一个端口和进程。更可靠的做法是单独跑一个 DERP 实例。
方案一:使用 Headscale 的 Dockerfile.derper 镜像
# 在 docker-compose.yml 中添加services: derper: image: headscale/headscale:0.29.1 container_name: derper restart: unless-stopped entrypoint: ["derper"] command: ["--hostname=hs.example.com", "--verify-clients=true"] ports: - "3478:3478/udp" volumes: - ./data/derper:/var/lib/headscale/derper networks: - headnet方案二:独立 DERP 部署(更推荐)
不想增加复杂度的话,直接用 Headscale 内置的 DERP 即可,轻度使用完全够用。
检查 DERP 状态
部署完 DERP 后,在 Headscale 容器中查看 DERP 节点列表确认是否正常工作:
docker compose exec headscale headscale debug derp输出示例:
Regions: 999 (selfhost): - node: selfhost-derp (STUN: 0.0.0.0:3478) IPv4: hs.example.com can dial: ✅ Home: 200ms看到 can dial: ✅ 就表明 DERP 节点正常工作。如果显示 ❌,检查防火墙是否开放了 UDP 3478 端口,以及域名解析是否正确。
你也可以在客户端上验证 DERP 延迟:
# 在客户端上查看 DERP 延迟tailscale status --json | jq '.Peer | to_entries[] | {name: .value.DNSName, relay: .value.Relay}'理解 DERP 的选择逻辑
Tailscale/Headscale 客户端在选择通信方式时遵循这个优先级:
- 直连(Direct):UDP 打洞成功 → 最佳性能,延迟最低
- 中继(Relay):NAT 穿透失败 → 走 DERP 中继,延迟稍高
- 不可达(Unreachable):中继也连不上 → 检查网络策略和防火墙
DERP 中继的带宽取决于 VPS 的带宽,所以如果大部分节点都是 relay 状态,建议升级 VPS 带宽或优化 NAT 穿透策略。
多 DERP 节点负载均衡
如果你的设备分布在不同地区,可以部署多个 DERP 节点,客户端会自动选择延迟最低的那个:
# 在不同 VPS 上部署独立的 DERP 实例,然后在 Headscale 配置中注册derp: server: enabled: true region_id: 999 region_code: "selfhost" region_name: "Self-hosted DERP" stun_listen_addr: "0.0.0.0:3478" # 手动添加其他 DERP 节点 urls: [] auto_update_enabled: false nodes: - region_id: 999 region_code: "selfhost" region_name: "Self-hosted DERP" # 可以添加多个节点 nodes: - name: "derp-asia" region_id: 999 hostname: "hs.example.com" stun_port: 3478 stun_only: false - name: "derp-eu" region_id: 1000 hostname: "eu.hs.example.com" stun_port: 3478 stun_only: false公网 DERP fallback
如果你想进一步保证连接成功率,可以加上 Tailscale 官方的公网 DERP 作为备选:
# config.yaml 中的 derp 配置derp: server: enabled: true region_id: 999 region_code: "selfhost" region_name: "Self-hosted DERP" stun_listen_addr: "0.0.0.0:3478" # 用官方的 DERP 列表作为 fallback urls: - https://controlplane.tailscale.com/derpmap/default auto_update_enabled: true这样即使你的自建 DERP 挂了,流量会自动切换到公网 DERP。
使用 headscale-ui 管理
浏览器打开 https://ui.hs.example.com,输入 Headscale API 地址:
API URL: https://hs.example.com在 Headscale 中生成 API Key:
docker compose exec headscale headscale apikeys create把生成的 Key 粘贴到 UI 中,就能可视化地管理所有节点了:
- 查看在线/离线设备
- 路由配置
- 用户管理
- 节点过期时间
运维技巧
自动清理过期节点
# 每小时清理过期节点docker compose exec headscale headscale nodes expire --all建议加到 crontab 中:
0 * * * * docker compose exec -T headscale headscale nodes expire --all日志与监控
日常运维中最常用的命令:
# 查看实时日志docker compose logs -f headscale
# 查看已注册节点docker compose exec headscale headscale nodes list
# 查看路由状态docker compose exec headscale headscale routes list
# 查看用户列表docker compose exec headscale headscale users list如果想可视化监控节点在线状态,headscale-ui 管理面板比命令行直观得多——一眼就能看出哪些设备离线了、最近一次在线是什么时候。
升级 Headscale 版本
cd /opt/headscale
# 拉取最新镜像docker compose pull headscale
# 重新创建容器docker compose up -d --force-recreate headscale
# 检查版本和迁移状态docker compose logs headscale | grep -i "migration\|version"Headscale 的数据迁移是自动执行的,升级前建议备份 data/ 目录:
tar -czf backup-$(date +%Y%m%d).tar.gz data/启用 ACL 策略
Headscale 支持基于标签的 ACL(访问控制列表),可以精确控制哪些设备能访问哪些设备:
acl_policy_path: /etc/headscale/acl.hujson创建 config/acl.hujson:
{ "groups": { "group:admin": ["tag:admin"], "group:dev": ["tag:dev"] }, "acls": [ {"action": "accept", "src": ["tag:admin"], "dst": ["*:*"]}, {"action": "accept", "src": ["tag:dev"], "dst": ["tag:dev:*"]} ]}备份配置
# 备份整个 headscale 数据目录tar -czf headscale-backup-$(date +%Y%m%d).tar.gz /opt/headscale/dataHeadscale vs Tailscale 对比
| 特性 | Headscale(自建) | Tailscale(官方) |
|---|---|---|
| 费用 | 仅付 VPS 费用(≈¥30/月) | 免费 3 用户,$6+/月起 |
| 用户数限制 | 无 | 3(免费版) |
| 数据隐私 | 完全自控 | 经官方中继 |
| DERP 中继 | 自建或公网 fallback | 全球节点 |
| 维护成本 | 需自己维护升级 | 零维护 |
| 官方客户端兼容 | ✅ 完全兼容 | 官方提供 |
| 稳定性 | 取决于 VPS | SLA 保障 |
| 管理面板 | headscale-ui(第三方) | 官方管理台 |
常见问题 FAQ
Q:为什么我用 tailscale status 看到是 relay 而不是 direct?
A:说明 NAT 穿透没成功,流量走了 DERP 中继。首先确认设备的 UDP 端口(3478)能被双方访问。如果双方都在严格 NAT 后面(如公司网络 + 手机热点),基本只能走 relay,但延迟通常也在可接受范围内。
Q:Headscale 的 IP 段为什么要用 100.64.0.0/10?
A:这是 IANA 预留的运营商级 NAT(CGNAT)地址段,与内网地址(192.168.x.x/10.x.x.x)不冲突,也不会和公网 IP 冲突,是 Tailscale/Headscale 的默认分配段。
Q:迁移已有的 Tailscale 设备到 Headscale 需要重装吗?
A:不需要重装。直接在设备上执行 sudo tailscale logout 退出官方网络,然后 sudo tailscale up --login-server=https://hs.example.com 重新登录即可。
Q:Headscale 支持 MagicDNS 吗?
A:支持。在 config.yaml 中启用 dns.magic_dns: true,然后在客户端 tailscale up --accept-dns 即可通过设备名互相访问(例如 ping laptop 而不是 ping 100.64.0.2)。
Q:能否把已有的 WireGuard 配置导入 Headscale?
A:不能直接导入。Headscale 的节点管理完全通过控制平面进行,需要每台设备用 Tailscale 客户端重新注册。不过你可以在 Headscale 中手动添加子网路由,让 Headscale 网络访问现有 WireGuard 子网。
Q:Headscale 支持 exit node(出口节点)吗?
A:支持。在要作为 exit node 的设备上执行 sudo tailscale up --login-server=https://hs.example.com --advertise-routes=0.0.0.0/0,::/0,然后在 Headscale 中启用路由即可。这样所有设备可以经过该节点访问公网,适合用来做科学上网或远程办公网络出口。
Q:我用的是 frp/Cloudflare Tunnel 暴露内网服务,和 Headscale 比哪个好?
A:场景不同。frp 和 Cloudflare Tunnel 适合暴露单个端口给公网(如把内网的 Web 服务发布出去)。Headscale 适合设备互联——让你的所有设备在一个虚拟局域网里互相访问。如果只是发布一个 Web 服务,frp 更简单;如果你需要 SSH 到任意设备、传输文件、访问内网资源,Headscale 更合适。
Q:Headscale 控制服务器宕机了,现有连接会断吗?
A:不会。Headscale 控制服务器只负责注册和信令交换(handshake),设备之间的 P2P 连接一旦建立,就不依赖控制服务器。即使控制服务器宕机了,已有连接照常工作。只是新设备无法注册加入网络,已有连接的保活机制会继续运行。
总结
Headscale 让你在 VPS 上自建一套完整的 P2P Mesh 组网服务,摆脱 Tailscale 免费版的 3 用户限制,同时数据完全由自己掌控。Docker Compose 部署方式让整个过程足够丝滑——从零到上线,半小时就能搞定。
有了 Headscale,你的所有设备——笔记本、服务器、树莓派、手机——都在一张网里。SSH 不用再开公网端口,文件传输用内网速度,数据库直连走加密通道。
部署清单回顾:
- ✅ 准备一台公网 VPS + 域名
- ✅ Docker Compose 部署 Headscale + Caddy + headscale-ui
- ✅ 创建用户和预授权密钥
- ✅ 客户端接入验证
- ✅ 可选:自建 DERP 中继 + ACL 策略
如果你在部署中遇到问题,欢迎评论区留言。
延伸阅读
- Docker 换源避坑指南 — 部署前的必备准备
- Nginx Proxy Manager 反代面板 — 统一管理所有服务的访问入口
- Uptime Kuma 自建服务监控 — 所有服务状态一目了然
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!