A WebAssembly-powered client for connecting to your Headscale nodes via SSH, VNC, or RDP — directly from the comfort of your browser. Includes an optional self-service integration for seamless device onboarding and management.
- SSH Console — Secure terminal access to your nodes
- VNC Viewer — Remote desktop viewing in the browser
- RDP Client — Secure access to Windows nodes, inspired by Cloudflare, without requiring a additional gateway
- Self-Service — Optional tsnet-based server allowing users to manage their own devices via an opt-in approach. It connects directly to the Headscale gRPC UNIX socket and requires minimal setup and maintenance.
- Stateless — Integrates into existing infrastructure without the need for a extra database or identity provider
CORS Restrictions:
- The console must be served from the same domain as Headscale or
- Headscale must return the correct
access-control-allow-origin
headers
typically handled via a reverse proxy
A minimal Docker image is available, featuring a Go web server to serve the static files or to run the self-service API.
docker run -it ghcr.io/rickli-cloud/headscale-console:unstable serve --help
docker run -it ghcr.io/rickli-cloud/headscale-console:unstable selfservice --help
latest
: Latest stable releasex.x.x
: Specific release versionsx.x.x-pre
: Pre-release versions (potentially unstable)unstable
: Built on every push to the main branch
A full deployment of traefik, headscale, headscale-console & headscale-selfservice can be found in docker-compose.yaml
.
-
Configure headscale in
config.yaml
-
Configure the environment in
.env
:# Required HEADSCALE_SERVER_HOSTNAME=headscale.example.com HEADSCALE_VERSION=0.26.0 # Optional HEADSCALE_CONSOLE_VERSION=unstable TRAEFIK_LISTEN_ADDR=0.0.0.0 TRAEFIK_VERSION=latest
-
Start it all up:
docker compose up -d
Important
When deploying for production it is recommended to use TLS between traefik and headscale.
Each release includes a downloadable ZIP archive with all required assets for deployment on static web servers (e.g., Nginx, Apache).
All assets are loaded relative to the initial URL, so it does not matter which path you serve the app from.
Configuration is completely optional
On startup the console tries to load ./config.json
. For the docker image you can mount /config.json
(or somewhere else defined with the configfile flag).
Currently still in beta and not yet fully documented. See src/lib/store/config.ts
for now.
Manual instructions are available in wasm/
. CI workflows also publish prebuilt WASM artifacts.
Requires WASM builds
Install dependencies:
deno install
Build the frontend:
deno task build
This could also be done inside a Docker container:
docker run -it --rm -v .:/work:rw --workdir /work --entrypoint /bin/sh denoland/deno:latest
Requires frontend build
docker build . -t headscale-console:custom
Requires frontend build
If you do not plan on running the console inside of docker you need to build the executable manually:
cp frontend.go.tmpl dist/frontend.go
go build main.go
This builds a native binary for your current OS and architecture. For other platforms, build natively or set appropriate cross-compilation flags.
The TCP connection (handled by Golang) is abstracted into a
IpnRawTcpChannel
on JS side. It implements theRTCDataChannel
interface to allow use with NoVNC & IronRDP but has nothing to do with WebRTC.
Go handles the full protocol stack. JavaScript handles rendering.
sequenceDiagram
DERP ->> Go: WebSocket
Go ->> JS: Text
JS ->> DOM: Render
JS -->> Go: Input
Go -->> DERP: WebSocket
Go handles the TCP layer. JavaScript (NoVNC) manages the VNC protocol.
sequenceDiagram
DERP ->> Go: WebSocket
Go ->> JS: TCP data
JS ->> DOM: Render
JS -->> Go: TCP data
Go -->> DERP: WebSocket
Go handles the TCP layer. JavaScript passes packets to the Rust-based WASM module, which handles TLS, RDP, and rendering.
sequenceDiagram
DERP ->> Go: WebSocket
Go ->> JS: TCP data
JS ->> Rust: TCP data
Rust ->> DOM: Render
JS -->> Rust: UI events
Rust -->> JS: TCP data
JS -->> Go: TCP data
Go -->> DERP: WebSocket
Reaches out via the DERP relay. Traffic is not encrypted with TLS (already protected by the underlying WireGuard tunnel).
sequenceDiagram
JS ->> Go: HTTP request
Go ->> DERP: WebSocket
DERP ->> Self-Service: WebSocket
Self-Service ->> Headscale : GRPC
Headscale -->> Self-Service : GRPC
Self-Service -->> DERP: WebSocket
DERP -->> Go: WebSocket
Go -->> JS: HTTP response
We value thoughtful feedback — whether it's about design decisions, usability, or ideas for improvement. You're welcome to open an issue to start a conversation. While we can’t promise to implement everything, we carefully consider all suggestions.
Contributions are welcome! However, to avoid wasted effort, please open an issue first to discuss any significant changes before submitting a pull request. Bug fixes, improvements, and well-scoped features are especially appreciated — just make sure they align with the project's direction.
MIT License - Copyright (c) 2025 rickli-cloud
Made with ❤️ for secure and hassle-free remote access.