用 Docker Compose 自建 Headscale,并接入手机和电脑

2026年8月30日| Ruichen Zhou| 约 15 分钟阅读

我的设备原来使用 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.yamlnetworks.controlsubnet 一致,修改其中一处时也要修改另一处。

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 后按下面的顺序操作:

  1. 打开右上角设置,进入 Accounts
  2. 打开右上角三点菜单,选择 Use an alternate server
  3. 输入 https://headscale.example.com
  4. 如果出现网页登录提示,先关闭它,回到 App;
  5. 再次进入 Accounts 的三点菜单,选择 Use an auth key
  6. 填入刚生成的一次性密钥;
  7. 回到首页,必要时点击 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 比较字段,再修改现有配置。跨多个次版本时按官方要求逐个次版本升级。

参考资料

评论