headscale-ui: a Svelte web frontend for your headscale coordination server
A web frontend for the headscale Tailscale-compatible coordination server
At a glance
- What is it?
- headscale-ui is a static SvelteKit frontend that talks to the headscale API from the /web path of the same subdomain. It installs as a container or a static bundle, and its main constraint is CORS, not features.
- Who is it for?
- Adopt headscale-ui if you already run headscale and want a browser view of nodes, users and API keys without touching the CLI for every change. Skip it if you cannot put it on the same subdomain as headscale or inject CORS headers through a reverse proxy, because the README states plainly that raw IPs and ports will not work.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Svelte, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The gap headscale-ui fills for headscale operators
headscale is a Tailscale-compatible coordination server, and it is operated through a command line and an API. Every node registration, route approval and key rotation goes through `headscale` commands or raw API calls. headscale-ui is a web frontend for that server. It is aimed at people who already run headscale and want a browser view of the same objects, rather than at people who have not installed headscale at all. The project is written in Svelte, ships under BSD-3-Clause, and the repository lists it as a web frontend in its own description, with topics covering sveltekit, tailwindcss and vpn-service. The README shows a demo GIF rather than a feature list, so the scope is best read from what the UI can do against the headscale API: manage nodes, users and API keys. It is not a VPN client and not a replacement for the Tailscale client on your devices. It is the control panel for the server those clients register with.
How the static frontend talks to headscale
The architecture is deliberately thin. headscale-ui is released as a static site, so there is no application backend of its own to operate. The browser loads the SvelteKit bundle and calls the headscale API directly. That is why the README insists the UI be served from the `/web` path: headscale occupies the rest of the domain, and the two must not overlap. It is also why CORS is the defining constraint of the whole project. The README links headscale issue 623 and states that headscale-ui must be served on the same subdomain, or CORS headers must be injected via reverse proxy. Authentication is a headscale API key that you create on the command line and paste into the UI's settings. The package.json confirms the shape of the build: `@sveltejs/adapter-static` is a dev dependency, and the `build` script is plain `vite build`. There is no server-side session store, no database, and nothing to back up beyond the static files. That is a real simplification for operators, and it also means every failure mode is a browser, CORS or API-key problem.
Installing headscale-ui with Docker Compose
The README's Docker example runs headscale and headscale-ui as two services. The headscale service mounts a config directory and a data directory and runs `serve`. The UI service uses the published image. Note the warning above the example: the latest major release moved the default container ports from 80 and 443 to 8080 and 8443, so existing compose files and Kubernetes manifests need their port mappings updated unless they set `HTTP_PORT` or `HTTPS_PORT` explicitly.
version: '3.5'
services:
headscale:
image: headscale/headscale:stable
container_name: headscale
volumes:
- ./container-config:/etc/headscale
- ./container-data/data:/var/lib/headscale
command: serve
restart: unless-stopped
headscale-ui:
image: ghcr.io/gurucomputing/headscale-ui:latest
restart: unless-stopped
container_name: headscale-uiThe two port variables the container accepts are `HTTP_PORT` and `HTTPS_PORT`, with `8080` and `8443` as the documented examples. The UI serves with a self-signed certificate by default, which is another reason a reverse proxy sits in front of it in practice. You also need a `config.yaml` under `container-config` so headscale itself has the settings it requires; the README points at the example config in the official headscale repository rather than duplicating it.
Once both containers are up, create an API key on the headscale side and paste it into the UI settings. The README gives both forms:
headscale apikeys create
docker exec <headscale container> headscale apikeys createIf the UI reports a missing bearer prefix, the key was not saved or the reverse proxy is not configured. That error message is the project's own diagnostic, and it points at configuration rather than at a bug.
Reverse proxy setup with Caddy and the /web path
Because the UI and the server share a domain, the proxy has to split traffic by path. The README's Caddy example sends anything under `/web` to the UI container on 8080 and everything else to headscale on 8080.
https://hs.yourdomain.com.au {
reverse_proxy /web* http://headscale-ui:8080
reverse_proxy * http://headscale:8080
}If you refuse to share a subdomain, the README documents a cross-domain Caddy configuration that answers preflight `OPTIONS` requests with `Access-Control-Allow-Origin`, `Access-Control-Allow-Headers` and `Access-Control-Allow-Methods`, and injects the same headers on responses from headscale. That block is longer and easy to get subtly wrong, which is the argument for using one subdomain. The README also points to a separate configuration document for other proxies such as Traefik, so a Traefik user should read that file rather than adapt the Caddy snippet. The blunt statement in the troubleshooting section is worth repeating: if you try to use raw IPs and ports, it will not work.
Where headscale-ui stops being the right tool
The README is unusually candid about testing scope. headscale-ui is only tested against the current stable version of headscale, Chrome and Chrome Mobile, and Firefox and Firefox Mobile. Mobile is checked for functionality, but the README states the web experience is not mobile optimised. If your operators work from tablets, that is a limitation you should weigh before standardising on it. The versioning table is the harder constraint. Each UI release maps to a minimum headscale version: 2026-03-17 and later for headscale 28 and above, 2025-05-22 for 26 and above, and so on down to releases before 2023-01-30 for headscale below 19. Running a mismatched pair is not supported by the table. The project also has no homepage listed and no documented rollback procedure in the README, so an upgrade that breaks against a newer headscale leaves you reading the release notes. Finally, if you never touch headscale from a browser and script everything through the API, the UI adds a second thing to proxy, certificate and keep patched for no operational gain.
headscale-admin and other frontends
The obvious alternative is headscale-admin, which appears repeatedly in the search terms people use around this project. The difference in approach is worth stating precisely, and the README does not compare itself to headscale-admin, so any claim about how that project is built would be speculation. What can be said from this repository is what headscale-ui commits to. It is a static SvelteKit bundle with a static adapter, no server component, and a hard requirement that it live at `/web` on the same subdomain as headscale or behind injected CORS headers. A frontend that instead ships its own backend process would not have that CORS constraint, because the browser would talk to its own origin and the backend would talk to headscale. That is the architectural fork to check when comparing options: where does the API call originate. If a candidate tool proxies headscale through its own server, the Caddy split above becomes unnecessary. If it is also a browser-to-API client, it inherits the same preflight behaviour and the same missing-bearer-prefix failure.
Maintenance, licensing and what an upgrade costs
The repository is not archived, and the last push was on 2026-03-16, which is recent enough that the project is being updated rather than left alone. Releases are dated, not semantic: 2026.03.17, 2025.08.23, 2025.07.12. That naming is useful because the version string itself tells you the release date, which pairs directly with the headscale compatibility table. The practical upgrade cost is low on the UI side, since it is a static bundle or a container image with two documented port variables. The cost sits on the headscale side: upgrading headscale past a compatibility boundary means upgrading the UI too, and the table is the only guide the README offers. There is no documented rollback path, so the safe move is to pin both images by tag rather than tracking `latest`, and to read the release notes before moving either. On licensing, headscale-ui is BSD-3-Clause, which is permissive and generally allows redistribution and modification with the licence and copyright notice retained; this is a description of the licence identifier, not legal advice, and anyone embedding it in a commercial product should read LICENSE.md and the project's SECURITY.md themselves.
Editorial conclusion
Adopt headscale-ui if you already run headscale and want a browser view of nodes, users and API keys without touching the CLI for every change. Skip it if you cannot put it on the same subdomain as headscale or inject CORS headers through a reverse proxy, because the README states plainly that raw IPs and ports will not work. Before deploying, check the versioning table against your headscale release, since UI version 2026-03-17 and later map to headscale 28 and above.
Frequently asked questions
What are the key differences between Headscale and Tailscale?
The README describes headscale as a Tailscale-compatible coordination server, and headscale-ui as a web frontend for it. The project does not document the differences between the two servers themselves, so this repository is not the place to answer that question.
Can Headscale be self-hosted?
Yes. The README's Docker example runs headscale and headscale-ui as containers, with headscale mounting a config directory and a data directory and running the serve command.
Is Tailscale self-hosted?
The README does not discuss Tailscale's own hosting model. It only states that headscale is a Tailscale-compatible coordination server and that headscale-ui is a frontend for headscale.
What is the difference between headscale ui and headscale admin?
The README does not compare headscale-ui with headscale-admin. What it documents about headscale-ui is that it is a static SvelteKit site served from the /web path on the same subdomain as headscale, or behind injected CORS headers.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/gurucomputing-headscale-ui)