Headscale 原生只有 CLI,每次加节点、批用户、看路由都得 SSH 敲命令。headscale-ui 是个纯静态 Web 前端,调 Headscale API 把这些操作搬到浏览器里。这篇记录它的安装、反代配置和登录踩坑。
系列:Tailscale 折腾 2 / 4
- 1 Tailscale 是什么:一篇看懂这套 P2P 内网穿透神器(折腾系列·序章)
- 2 给 Headscale 配个 Web 界面:headscale-ui 安装折腾记 当前
- 3 自建 DERP 中继:让 Headscale 打洞失败也能稳连国内 20ms
- 4 把设备接进自建 Headscale:从 tailscale up 登录到 headscale-ui 管理
折腾背景:序章里我把 Tailscale / 自建 Headscale 的原理讲过了,假设你已经有一套 Headscale 跑起来。但 Headscale 原生只有命令行——加节点、批准设备、建用户、配子网路由,全得 SSH 上去敲
headscale命令。设备一多就烦。headscale-ui 是社区做的一个纯静态 Web 前端,直接调 Headscale 的 API,把这些操作搬进浏览器。这篇记一下它的安装和两个反代/登录的坑。
它是怎么工作的
headscale-ui 本身没有后端,就是一堆静态 HTML/JS。它在你浏览器里,拿着你填的 API Key 直接去敲 Headscale 的 HTTP API。所以部署的核心其实是两件事:
- 把这堆静态文件托管起来(Docker 或塞进 nginx 目录)。
- 用反向代理把"静态 UI"和"Headscale API"放到同一个域名下,避免浏览器跨域(CORS)。
flowchart LR
U["浏览器
headscale-ui"]
P["反向代理
nginx / Caddy"]
UI["静态文件
/web"]
API["Headscale API
:8080"]
U -->|"访问 /web"| P
P -->|"① 静态页面"| UI
U -->|"带 API Key 调接口"| P
P -->|"② 转发 API"| API
style P fill:#dbe9ff,stroke:#2563eb,stroke-width:2px
全流程总览
flowchart TD
A["① 跑起 headscale-ui
Docker / 静态文件"] --> B["② 生成 API Key
headscale apikeys create"]
B --> C["③ 反代:同域暴露
/web + API"]
C --> D["④ 浏览器开 /web
填 URL + API Key"]
D --> E["⑤ 验证:能看到节点列表"]
style C fill:#ffe9d5,stroke:#b71d18
标橙的第③步(反代同域)是最容易卡住的——跨域和 HTTPS 没配对,UI 会一直转圈连不上 API。
前置
- 一台已经在跑的 Headscale(本文假设它监听在本机
:8080,对外域名vpn.e7coding.com)。 - 一个能签证书的反代(nginx 或 Caddy),UI 强烈建议走 HTTPS。
第 1 步:把 headscale-ui 跑起来
两种方式选一种。
方式 A:Docker(推荐,省事)
docker run -d \
--name headscale-ui \
--restart unless-stopped \
-p 9443:443 \
ghcr.io/gurucomputing/headscale-ui:latest容器里就是个装好静态文件的小 web server,监听 443(映射到宿主 9443)。后面反代把 /web 指过来即可。
方式 B:静态文件 + nginx(想完全自己掌控)
去 Releases 下最新的发布包,解压到 nginx 的站点目录:
mkdir -p /var/www/headscale-ui
unzip headscale-ui.zip -d /var/www/headscale-ui然后让 nginx 把 /web 映射到这个目录(见下一步的配置)。
第 2 步:生成 API Key
headscale-ui 靠 API Key 鉴权,在 Headscale 服务器上生成一个:
# 创建一个有效期较长的 key(按需调整)
headscale apikeys create --expiration 999d
# 输出形如:
# xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy把这串 key 完整复制下来,等会儿要填进 UI。它只在创建时完整显示一次。
# 顺手可以列出 / 过期废弃旧 key
headscale apikeys list第 3 步:反向代理,同域暴露 UI 和 API(关键)
核心是把 /web(UI)和 Headscale 的 API 路径放在同一个域名下。下面是 nginx 的示意配置(用方式 A 的容器为例):
server {
listen 443 ssl;
server_name vpn.e7coding.com;
ssl_certificate /etc/nginx/certs/vpn.e7coding.com.crt;
ssl_certificate_key /etc/nginx/certs/vpn.e7coding.com.key;
# ① Web 界面:转发到 headscale-ui 容器
location /web {
proxy_pass https://127.0.0.1:9443;
proxy_set_header Host $host;
}
# ② Headscale API / 控制面:转发到 headscale
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}用方式 B(静态文件)的话,把
location /web换成alias /var/www/headscale-ui;直接由 nginx 出静态文件即可,其余不变。重载:nginx -t && systemctl reload nginx。
第 4 步:打开 UI,填入配置
浏览器访问:
https://vpn.e7coding.com/web/进设置页,填两样东西:
- Headscale URL / API URL:
https://vpn.e7coding.com(你的对外域名,不要带 /web) - API Key:第 2 步生成的那串
保存后,正常就能看到节点(devices)列表了。我自己最终落在 https://vpn.e7coding.com/web/devices.html 这个页面管设备。
第 5 步:验证
能看到节点列表、能批准/删除设备、能建用户,就算通了。常用的几件事现在都能点鼠标搞定:
- 批准新设备入网(配合 DERP 那篇里
tailscale up后生成的 register 链接) - 建 / 删用户
- 给节点配子网路由、打 tag
- 管理、废弃 API Key
踩坑小结
- 跨域(CORS):UI 和 API 必须同域。如果 UI 在
a.com、API 在b.com,浏览器会拦截请求,UI 一直连不上。用反代把它们收到一个域名下是最稳的做法。 - HTTPS 几乎是必须:很多浏览器对混合内容(HTTPS 页面请求 HTTP 接口)会直接拦。UI 走 https,API 也走 https,省一堆事。
- API Key 只显示一次:创建时没存下来,就只能废弃重建。
- URL 别填成 /web:设置里填的是 Headscale 的根域名,不是 UI 自己的
/web路径,填错就一直 401 / 连接失败。 - API Key 会过期:
--expiration到期后 UI 突然全部失效、报鉴权错,记得提前headscale apikeys create续一个再去 UI 里换上。
有了 Web 界面,下一篇就能在 headscale-ui 里点几下,把自建 DERP 节点批准入网了 👉 自建 DERP 中继。