headscale-admin is a static browser console for a self-hosted Tailscale control server
Admin Web Interface for juanfont/headscale
At a glance
- What is it?
- headscale-admin is a SvelteKit web interface for headscale, built with the Skeleton framework and shipped as static files that talk to the headscale API straight from the browser. Its security model is stateless by construction, which removes the need for sessions and puts the whole burden on protecting the API token, and its one sharp edge is the build time ENDPOINT variable.
- Who is it for?
- headscale-admin suits a self hosting operator who runs headscale and wants a graphical way to manage nodes, users and routes without scripting the API, and it suits a team that would rather host a static bundle than maintain a server side application.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 41 days 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 October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A GUI for a control server you already run
headscale-admin is described as a simple, modern and useful web interface for juanfont/headscale, which is an open source, self-hosted implementation of the Tailscale control server. The relationship is worth stating plainly: this is not a fork of headscale and not a replacement for it, it is a client for the headscale API that someone else maintains.
That matters because headscale is the component that holds the coordination state for a tailnet. Without it there is no way to introduce a device, revoke a key or see the machine list. The headscale project offers a command line interface for all of that, and this project exists for the people who would rather click.
The project is candid about its status. headscale-admin is still in active development and will evolve in tandem with headscale, and it should not be treated as a final product, though when used properly it is considered safe to run in a production environment. The known issues section currently lists no known issues, which is a statement about the tracker rather than a promise about the code.
The technology stack is visible in the repository. It is a SvelteKit application with the Skeleton framework on top of TailwindCSS, and it uses the svelte/adapter-static adapter so the build produces only static files. There is no server side runtime, no database and no backend of its own, which is the single fact that explains the whole security model and most of the deployment guidance.
The audience is a self hosting operator with a headscale deployment, usually on a small server, who wants an administration interface they can point a browser at.
Stateless by construction, so the API token is the whole risk
The security section is the most unusual thing about this project, and it is worth reading in full before deploying it.
headscale-admin is an entirely stateless application. The static files hosted on a server do not perform any interaction with the headscale API or backend. It only provides the application scaffolding that facilitates interactions from the client's browser to the headscale API. There are no sessions, no API tokens, and no other sensitive information passed to or from the web server hosting it. The project's conclusion is that security beyond SSL certificates is unnecessary, though you may choose to add it simply to hide the existence of the application. Any static headscale user interface offers functionality that can be trivially replicated with cURL or other web request utilities.
So the security of the system reduces to one sentence the project states explicitly: the security of headscale lies in your API token being securely preserved. And the token is stored on your browser, not in cookies or session information, which is why the recommendations are about transport and headers rather than about access control.
Three recommendations follow from that. Use proper TLS, for example with a frontend proxy, to protect the application from HTML injection in an active man in the middle attack, and this is highly recommended. If you host headscale-admin on a different subdomain than headscale, allow the headscale-admin domain in the CORS headers of headscale, or the browser will block the requests. And password protection or SSO is not required, though obscuring the fact that you run an admin utility can add anonymity, while doing nothing to stop an attacker who already has a compromised key.
That last point is the honest limit of the design. The project is not a security boundary, it is a convenience, and the convenience itself is what a stolen API key would give an attacker.
ENDPOINT is decided at build time, not at run time
The one configuration detail that will cost you an hour if you skip it is the ENDPOINT variable, and it is a build time setting rather than a runtime one, because the output is a static bundle.
If you build headscale-admin from source and want to host it on an endpoint other than the base of the domain, for example headscale.example.com/admin, you must set the ENDPOINT environment variable when building. Otherwise it defaults to expecting to be hosted on the root path, and the redirects and resource loading will not work correctly if you place the files in a child folder.
The second half of the trick is that the build output has to be named to match. Once built, it is recommended to rename the build directory to the same name as your ENDPOINT variable so the requests follow the folder structure and do not have to be stripped or rewritten by a frontend proxy. If you used ENDPOINT=/admin, then rename the build directory as admin and place it in the root of your hosting. If you are hosting on the root path with no ENDPOINT variable, the contents of the build directory go directly into the root path.
The Docker side has the same shape, and the default in the container is always /admin, chosen because it does not clash with any URL endpoints provided by the headscale API and so can safely be hosted on the same subdomain. Building for a different endpoint uses a build argument:
# Using a non-default value (/manager)
docker build . -t headscale-admin --build-arg ENDPOINT=/managerWith the default endpoint the same command works without the argument; for the root path, pass an empty one with --build-arg ENDPOINT=. The Dockerfile implements this by copying the built output into a directory named after the ENDPOINT inside the final image, so the container serves the files at the right path with no rewriting layer.
Cloning, setting ENDPOINT and building
The build path is short, and the documentation gives both a shallow clone for a specific version and a full one:
git clone --depth 1 --branch <version> https://github.com/GoodiesHQ/headscale-adminAlternatively you can clone the main branch for the latest release, or use git checkout <version> afterwards for a specific version. The shallow clone with a branch is the one to use when you are deploying a pinned release, since it avoids pulling the whole history for a bundle of static files.
The endpoint is then exported for the shell you are building in, and the documentation gives the three forms for the three common shells:
export ENDPOINT="/admin"with set ENDPOINT=/admin for cmd.exe and $env:ENDPOINT=/admin for PowerShell. The Docker container uses the /admin endpoint by default, which is why that is the value in the examples.
The production build is one command:
npm run buildwhich produces the build directory that expects to be hosted at the provided ENDPOINT relative to the root domain. The package manifest shows the surrounding tooling: a dev script running vite dev, a preview script, a check script that runs svelte-kit sync and then svelte-check against the tsconfig, a check:watch variant, tests with vitest, and lint and format scripts that run prettier and eslint. Everything is TypeScript with Svelte 5, Vite 6, Tailwind 3 and Vitest 3, and the front end dependencies are modest and legible, with dompurify for sanitising HTML, highlight.js and svelte-jsoneditor for rendering, json5 for parsing configuration with comments, ipaddr.js and js-xxhash for the address and hashing work the interface needs, and async-mutex for serialising operations.
The container serves with Caddy and expects a proxy in front
The recommended deployment is Docker, and pre-built images are provided that always use the /admin endpoint.
The image itself is two stages. The build stage is node:20-alpine, it installs the dependencies with npm install, copies the configuration files, the static assets and the source, and runs npm run build with the ENDPOINT variable already set as an environment variable in the stage. The final stage is caddy:latest rather than a plain web server, and the built output is copied into a directory named after the endpoint inside the image, with the repository's Caddyfile copied to /etc/caddy/Caddyfile and the command caddy run with that config.
That choice is the reason the image is port 80 and expects a frontend proxy to handle SSL certificate management. The image serves plain HTTP, and the documentation is explicit that a front end proxy service is expected to perform the certificate work. Running it locally is:
docker run -p 8000:80 headscale-adminand the pre-built image from Docker Hub runs the same way:
docker run -p 8000:80 goodieshq/headscale-admin:latestThe repository ships an example/ directory with a Caddyfile, a docker-compose.yml and a headscale-conf/ directory, which is the fastest way to see the intended deployment: headscale and headscale-admin together, behind Caddy, with the CORS configuration that the two of them need. Given the security notes above, that example is worth reading in full before you improvise, because the CORS allowance is the part that is easy to forget and impossible to debug from the browser console when it is missing.
Docker tags now track headscale major versions
The versioning story changed at version 0.24, and it is a decision worth understanding before you pick a tag.
Due to the dynamic nature of the headscale API, starting with version 0.24 headscale-admin no longer attempts to maintain compatibility with multiple versions of headscale. Previously there was a Legacy API option. Instead, headscale-admin versions track the major versions of headscale itself, so the tag goodieshq/headscale-admin:0.24 is compatible with version headscale/headscale:0.24 of headscale. Minor versions do not track headscale, because there may be a need for bug fixes outside of headscale's release schedule.
The latest tag is defined narrowly: goodieshq/headscale-admin:latest is only used for the latest stable release of headscale-admin that is compatible with a recent stable release of headscale. Since those are not guaranteed to line up, the documentation recommends explicitly tagging the major version of headscale you are using.
That is the whole upgrade story, and it is a simple one: match the major version numbers, and expect the interface to change when headscale's API changes in a breaking way. For a self hosted operator this is a feature rather than a bug, because the alternative, an admin interface that silently speaks the wrong API version, is worse.
The release history is compressed in a way that suggests a batch. v0.26.0, v0.27.0 and v0.28.0 were all tagged on 2026-09-12, within the same day, and the last push to the repository was on that date as well. The project is not archived, so it is alive, and the project description's own statement is the best summary of what to expect: it evolves in tandem with headscale and should not be treated as a final product. The licence is GPL-3.0, which matters for anyone who wants to modify and redistribute the interface, since a derivative has to be published under the same terms.
Editorial conclusion
headscale-admin suits a self hosting operator who runs headscale and wants a graphical way to manage nodes, users and routes without scripting the API, and it suits a team that would rather host a static bundle than maintain a server side application. It is the wrong choice if you want the admin interface protected by its own login, because the project states that password protection or SSO is not required and that the API key lives only in the browser, and a poor match if you cannot serve it over TLS behind a proxy. Before deploying, set the ENDPOINT variable at build time and name the build directory to match, allow your admin domain in the headscale CORS headers, and pin the major version tag that matches your headscale release rather than tracking latest.
Frequently asked questions
Does headscale-admin need a server side backend?
No. It is an entirely stateless application: the static files do not interact with the headscale API themselves, there are no sessions, and no API tokens or other sensitive information are passed to or from the web server. The browser talks to headscale directly.
How do I host headscale-admin on a sub-path such as /admin?
Set the ENDPOINT environment variable at build time, build with npm run build, then rename the build directory to admin and place it at the root of your hosting. With Docker, the default endpoint is already /admin, and a different value is passed as a build argument.
Which headscale-admin image tag should I use?
Since version 0.24 the tags track headscale major versions, so headscale-admin:0.24 matches headscale/headscale:0.24. Minor versions do not track headscale, and the latest tag only points at a release compatible with a recent stable headscale, so the project recommends pinning the major version explicitly.
Is headscale-admin protected by a login or by SSO?
No, and the project says so directly. Password protection or SSO is not required to secure it, and the API key information is stored only in the browser rather than in cookies or session information. The security of the underlying headscale instance depends on keeping that key safe, and TLS plus correct CORS headers are the recommended protections.
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/goodieshq-headscale-admin)Was this useful?