Headscale 与 Headpanel 部署指南
本文介绍如何在一台 Debian/Ubuntu 服务器上部署 Headscale 和 Headpanel,并使用 Caddy 提供 HTTPS。示例使用以下占位符,请全部替换为自己的值:
headscale.example.com:Headscale 控制服务域名panel.example.com:Headpanel 管理面板域名/opt/headpanel:Headpanel 代码目录
推荐为 Headscale 和 Headpanel 使用两个独立子域名。这样 Headscale 独占根路径,
Headpanel 也不需要设置 Next.js basePath。
1. 部署结构
本教程默认使用 API-only 模式:Headpanel 通过 Headscale REST API 管理节点、用户和 预授权密钥,不直接修改 Headscale 主机配置。只有确实需要在面板中修改 Headscale 网段并重启服务时,才启用主机控制模式。
环境要求
- Ubuntu 22.04+ 或 Debian 12+
- 一个可使用
sudo的账号 - 两个已解析到服务器公网 IP 的域名
- 防火墙开放 TCP 80/443
- 若启用 Headscale 内置 DERP/STUN,按实际配置开放 UDP 3478
2. 安装 Headscale
Headscale 官方推荐 Debian/Ubuntu 使用发布页提供的 DEB 包。先在
Headscale Releases 找到稳定版本,版本号填写时不要带 v:
DEB 包会安装 Headscale 用户、示例配置和 systemd 服务。不要用下面的片段覆盖完整配置; 请编辑包内提供的配置,并保留当前版本示例中的其他必需字段:
确认或调整以下字段:
包内最新完整示例通常位于:
启动并检查 Headscale:
当 server_url 是 HTTPS、Headscale 自身未配置证书时,日志可能提示正在无 TLS 监听;
如果 TLS 确实由 Caddy 终止,这是预期行为。
3. 创建 Headscale API key
Headpanel 需要 Headscale API key。API key 默认有效期由当前 Headscale 版本决定; 生成后只显示一次,请立即保存到密码管理器:
先在本机验证 key。不要把真实 key 写入 shell 历史,可临时交互读取:
管理 API key:
4. 安装 Node.js 24 和 Headpanel
Headpanel 需要 Node.js 24,并通过项目锁定的 pnpm 版本构建。用什么装不限 —— 系统包、 mise、fnm、nvm 都行,唯一的要求是 root 运行部署脚本时能找到 Node.js 24 和 pnpm。
下面以 mise 为例(它能同时管住 node 和 pnpm,且 pnpm 版本会跟随项目的
packageManager 字段):
用系统包也可以,Ubuntu 24.04 起 apt 的 nodejs 已经是 24:
如果之后要配 systemd 服务,不要把版本号写进路径。 像
/root/.local/share/fnm/node-versions/v24.15.0/installation/bin/node 这样的写法,
node 一升级服务就起不来。用版本管理器提供的稳定入口(mise 是
/root/.local/share/mise/shims/pnpm),或者系统包的 /usr/bin/node。
克隆项目并创建外置配置、数据目录:
生成面板会话密钥和初始管理员密码:
编辑运行时环境文件:
同机、独立子域名、API-only 模式的推荐配置:
确保只有 root 可以读取:
构建、安装 systemd 服务并执行本机健康检查:
检查服务:
可选:启用同机主机控制
主机控制允许面板修改 /etc/headscale/config.yaml 并执行 systemctl restart headscale,
权限明显高于 API-only 模式。只有确实需要“网段设置”功能时才启用:
当前部署脚本默认以 root 运行面板服务,能满足这些本机操作。远程部署的面板应保持
HEADSCALE_HOST_CONTROL=false。
5. 配置 Caddy 与 HTTPS(推荐)
Caddy 会自动申请、续期 TLS 证书,并原生支持反向代理升级连接。使用官方 Debian/Ubuntu 软件源安装稳定版:
编辑 Caddyfile:
独立子域名的推荐配置:
格式化、校验并平滑加载:
只要域名已经解析到服务器并且 TCP 80/443 可访问,Caddy 会自动获取公开受信任的证书。
验证公网入口:
Headscale 使用自定义的 Tailscale Control Protocol。Caddy reverse_proxy 支持升级连接,
上面的 Headscale 配置来自官方 Caddy 示例。不要把 Headscale 放在 Cloudflare 橙云代理
或 Cloudflare Tunnel 后面。
可选:把面板挂载到 /panel
独立子域名更简单。如果必须使用 https://headscale.example.com/panel,先修改环境文件:
HEADPANEL_BASE_PATH 会在构建期写入 Next.js 路由,修改后必须重新执行部署脚本。将
Caddyfile 中两个独立 HTTPS 站点替换为:
保留前面的 http://headscale.example.com 块,以便 /generate_204 返回 204 并将其他
HTTP 请求重定向到 HTTPS。
6. 首次登录与连接节点
访问 https://panel.example.com/login,使用环境文件中的 ADMIN_USERNAME 和
ADMIN_PASSWORD 登录。管理员只会在面板数据库为空时自动创建。
创建第一个 Headscale 用户:
在客户端安装 Tailscale 后发起注册:
终端或浏览器会显示 Auth ID,在服务器审批:
也可以在 Headpanel 中创建预授权密钥,并使用面板生成的安装命令连接节点。
ACL 策略基线
增删组时 Headpanel 会重写 Headscale 的 ACL policy:每个组贡献一条 tagOwners 和一条
acls 规则,使组内节点互通、且仅限组内。policy 里其余的内容 —— tag:approved 的
归属、子网路由器所服务的网段、你手写的任何规则 —— 都来自一个基线文件,面板把自己的规则
合并在它之上。
创建第一个组之前先建好它:
把 admin@ 换成拥有现有节点的 Headscale user,并把子网路由器宣告的每个网段都列进
dst。已批准但没写进 dst 的路由会进入路由表、再被 policy 丢弃 —— 表现为面板里显示
已批准,网段里却什么都连不通。
没有该文件时基线为空:组功能照常可用,但第一次增删组就会把线上 policy 替换成只剩面板 管理的那部分。文件存在却读不出或解析失败时,组操作会直接报错,而不是下发一份丢了基线的 policy。
用 HEADPANEL_POLICY_BASELINE 可以改变该文件位置。
默认区
组是可选的:不建组时,节点和 key 都归「默认区」。基线里那个 admin@ 和
tag:approved 正是默认区的两个值 —— 面板发组外的 key 时把它挂在这个 user 名下,
审批组外的节点时给它打这个 tag。两者分别由 HEADPANEL_DEFAULT_HS_USER 和
HEADPANEL_APPROVED_TAG 指定,默认就是 admin 和 tag:approved。
改动其中任何一个,都要同步改基线:user 必须在 Headscale 里真实存在,tag 必须在基线的
tagOwners 里由该 user 拥有,否则组外的节点接入后会被 ACL 隔离。
7. 更新与备份
更新 Headpanel
更新 Headscale
升级前备份配置和数据,然后安装目标版本的新 DEB 包。务必阅读对应版本的官方升级说明,
并将现有配置与新版本 config-example.yaml 对比:
建议备份内容
/etc/headscale/config.yaml/var/lib/headscale//etc/headpanel/headpanel.env/var/lib/headpanel/headpanel.db/etc/caddy/Caddyfile和证书恢复方案
环境文件、API key 和 SQLite 数据库都不应提交到 Git。
8. 常见问题
GET /node -> 404
Headscale REST API 前缀是 /api/v1。测试节点接口应使用:
HEADSCALE_API_URL 可以填写 https://headscale.example.com,也可以填写完整的
https://headscale.example.com/api/v1,不要填写以 /node 结尾的地址。
API 返回 401 或 403
- 重新执行
headscale apikeys list检查 key 是否过期。 - 确认 API key 没有多余引号、空格或换行。
- 使用 Bearer 请求直接测试
/api/v1/user。 - API key 丢失后无法重新读取,应使旧 key 过期并创建新 key。
面板返回 502
重点检查 Node.js 是否为 24、环境文件权限、HEADPANEL_BASE_PATH 是否与代理路径一致,
以及修改 basePath 后是否重新构建。
Headscale 客户端无法连接
同时检查 Caddy 日志,并确认请求没有经过不支持 Tailscale Control Protocol POST 升级的 CDN 代理:
组管理列表为空
Headpanel 的“组”是面板本地的权限映射,不会自动把所有 Headscale 用户导入为组。 先在 Headscale 创建用户,再在 Headpanel 的组管理页面创建对应组。