用 Docker Compose 自建 Headscale,并接入手机和电脑
我的设备原来使用 Tailscale 官方控制服务器。需要管理的设备变多以后,我把控制服务器换成了自建 Headscale,Tailscale 客户端仍然安装在电脑、手机和路由器上。
Headscale 负责设备注册、地址分配、名称和访问规则。设备之间能直连时,数据不会经过 Headscale;直连失败时,客户端才会通过 DERP 中继服务器转发流量。自建方法见DERP 部署文章,这篇先把 Headscale 和客户端接入写完整。
下面使用 Headscale 0.29.3、Docker Compose、SQLite 和 Caddy。域名、地址和用户名都是示例,部署时要换成自己的值。
1. 准备域名和服务器
需要准备:
- 一台安装了 Docker 与 Docker Compose 的 VPS;
- 一个域名,例如
headscale.example.com; - 域名的 A 或 AAAA 记录指向 VPS;
- 公网可以访问 80/TCP 和 443/TCP;
- 一个分配给 Tailnet(自己的虚拟内网)的地址段。
Headscale 分配的 IPv4 地址必须位于 100.64.0.0/10。这篇使用 100.64.10.0/24。MagicDNS 会为设备名提供内网解析,它使用的域名要与 Headscale 服务域名不同。例如服务地址是 headscale.example.com,MagicDNS 可以用 tail.example.com。
2. 创建目录并下载同版本配置
先创建目录:
sudo mkdir -p /opt/headscale/headscale/data
sudo mkdir -p /opt/headscale/caddy/data
sudo mkdir -p /opt/headscale/caddy/config
cd /opt/headscale
配置文件要与运行版本一致。这里固定使用 0.29.3:
sudo curl -fsSL \
https://raw.githubusercontent.com/juanfont/headscale/v0.29.3/config-example.yaml \
-o /opt/headscale/headscale/config.yaml
main 分支可能已经加入当前稳定版不认识的字段,因此这里按版本标签下载。
3. 编写 Docker Compose 配置
创建 /opt/headscale/compose.yaml:
name: headscale
services:
headscale:
image: docker.io/headscale/headscale:0.29.3
container_name: headscale
restart: unless-stopped
command: serve
read_only: true
tmpfs:
- /var/run/headscale
ports:
- "127.0.0.1:8080:8080"
- "127.0.0.1:9090:9090"
volumes:
- ./headscale/config.yaml:/etc/headscale/config.yaml:ro
- ./headscale/data:/var/lib/headscale
healthcheck:
test: ["CMD", "headscale", "health"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
networks:
control:
caddy:
image: docker.io/library/caddy:2.11.4-alpine
container_name: headscale-caddy
restart: unless-stopped
depends_on:
headscale:
condition: service_healthy
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
networks:
control:
networks:
control:
name: headscale-control
ipam:
config:
- subnet: 172.30.100.0/24
8080 和 9090 只监听本机。公网请求由 Caddy 接收,再转发到 Docker 网络中的 Headscale。
4. 修改 Headscale 配置
打开 /opt/headscale/headscale/config.yaml。官方示例已经包含所有字段,先修改下面这些值:
server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
trusted_proxies:
- 172.30.100.0/24
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v4: 100.64.10.0/24
v6: fd7a:115c:a1e0:10::/64
allocation: sequential
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
policy:
mode: file
path: ""
dns:
magic_dns: true
base_domain: tail.example.com
override_local_dns: false
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
trusted_proxies 的网段要与 compose.yaml 中 networks.control 的 subnet 一致,修改其中一处时也要修改另一处。
policy.path 留空时使用默认规则,所有设备可以互通。先让设备接入并确认连接正常,再按官方文档增加 ACL 或 Grants(访问控制规则)。一次只改一类配置,连接失败时才能看出是哪项改动造成的。
5. 配置 Caddy 和 HTTPS
创建 /opt/headscale/caddy/Caddyfile:
headscale.example.com {
reverse_proxy headscale:8080
}
Caddy 会为域名申请证书。启动前确认域名已经指向这台 VPS,80 和 443 没有被其他程序占用。
6. 启动 Headscale
先让容器读取配置并检查语法:
cd /opt/headscale
sudo docker compose run --rm headscale configtest
检查通过后启动:
sudo docker compose up -d
sudo docker compose ps
再检查本机和公网地址:
curl -fsS http://127.0.0.1:8080/health
curl -fsS https://headscale.example.com/health
两条请求都正常后再注册设备。公网 /health 不通时,依次检查域名解析、证书、80/443 防火墙和 Caddy 日志;客户端此时无需改动。
7. 创建用户和一次性密钥
Headscale 中的设备需要归属于一个用户。先创建用户:
sudo docker exec headscale headscale users create home
sudo docker exec headscale headscale users list
users list 会显示用户 ID。使用这个 ID 创建一次性密钥:
sudo docker exec headscale \
headscale preauthkeys create --user <用户ID> --expiration 30m
这枚密钥 30 分钟内有效且只能使用一次。每台设备接入时单独生成一枚。
8. Android 使用一次性密钥接入
Android 使用官方 Tailscale App,安装来源可以是 Google Play 或 F-Droid。打开 App 后按下面的顺序操作:
- 打开右上角设置,进入
Accounts; - 打开右上角三点菜单,选择
Use an alternate server; - 输入
https://headscale.example.com; - 如果出现网页登录提示,先关闭它,回到 App;
- 再次进入
Accounts的三点菜单,选择Use an auth key; - 填入刚生成的一次性密钥;
- 回到首页,必要时点击
Log in。
连接成功后,Headscale 中会出现这台 Android 设备。
9. iPad 使用网页注册
iPad 安装 App Store 中的 Tailscale。打开 App 后进入登录页面,在右上角菜单中选择 Use custom coordination server,输入 https://headscale.example.com。
浏览器页面会显示用于注册的 Auth ID。在 Headscale 服务器上执行:
sudo docker exec headscale \
headscale auth register --user home --auth-id <AUTH_ID>
注册完成后回到 iPad,App 会自动连接。Auth ID 仅本次注册有效。
10. Linux 和 macOS 使用命令接入
Linux 或可以使用 Tailscale CLI 的 macOS,可以直接使用一次性密钥:
sudo tailscale up \
--login-server https://headscale.example.com \
--authkey <一次性密钥> \
--hostname <设备名>
如果不想把密钥放进命令历史,可以使用网页注册:
sudo tailscale up --login-server https://headscale.example.com
浏览器显示 Auth ID 后,在服务器执行与 iPad 相同的 headscale auth register 命令。
11. 查看、重命名和删除节点
列出节点:
sudo docker exec headscale headscale nodes list
需要改名时,先从列表找到节点 ID:
sudo docker exec headscale \
headscale nodes rename phone-server --identifier <节点ID>
设备重装或重新注册后,先确认新节点可以连接,再删除旧节点:
sudo docker exec headscale \
headscale nodes delete --identifier <旧节点ID>
如果 SSH 或 Uptime Kuma、Beszel 之类的监控服务直接填写了旧的 100.x 地址,也要一起修改。手机服务器的处理过程见手机接入 Tailscale、SSH 和监控。
12. 备份和升级
Headscale 使用 SQLite,数据库和 Noise 私钥都在 /opt/headscale/headscale/data。升级前先停止服务并备份整个目录:
cd /opt
sudo docker compose -f headscale/compose.yaml down
sudo cp -a headscale "headscale.backup-$(date +%Y%m%d%H%M%S)"
sudo docker compose -f headscale/compose.yaml up -d
此时服务仍按原版本运行,/opt 下多了一份带时间戳的完整备份。
升级镜像的同时也要更新配置:先阅读目标版本的发布说明,下载同版本的 config-example.yaml 比较字段,再修改现有配置。跨多个次版本时按官方要求逐个次版本升级。