A self-hosted web panel for headscale — the open-source implementation of the Tailscale coordination server.
Your own Tailscale control server, with a real admin panel. Add a machine, grant
access, send traffic through a chosen node — no editing policy.hujson, no SSH
into nodes. The panel does not replace headscale and does not proxy traffic: it
takes over the part that is otherwise done by hand.
Русская версия · Why and what for · Changelog · noderoost.ru
![]() Access — who reaches which server, on which port |
![]() Routing — who goes where, through which node |
![]() Server detail — roles, subnets, exit gateway, agent |
![]() Sign-in — password and a second factor |
Screens are rendered from the panel's own stylesheet with example data; the addresses are documentation ranges (RFC 5737), not a live tailnet.
The panel does not replace headscale and does not carry your traffic. It takes over
the part that is otherwise done by hand: granting access becomes a who → where → on
which port rule instead of an edit to policy.hujson; a subnet behind a node is a
button instead of approve-routes by node id; a route is applied by an agent instead
of an SSH session on every machine.
What people build with it — two office LANs seeing each other without a public address anywhere, a work machine reachable from behind someone else's NAT, a database open to a contractor on one port and nothing else, a laptop leaving for the internet through a server whose address a partner allows. Reasons, goals and the full set of scenarios → docs/why.md
Servers and devices. Nodes split into servers (things you connect to) and personal devices. Devices are isolated from one another and can never be a rule's target — enforced in the policy engine, not in the UI, because a rule can also arrive through the API.
Grants, not an ACL file. A rule is who → where → on which port. Grant it by clicking, to several nodes at once, or from inside a node's own page. The HuJSON is generated, pushed, and rolled back if headscale refuses it.
Roles. A role is a group of servers (a headscale tag). Grant access to the role; adding a server to it later needs no rule changes.
Routing. Directions — these nodes reach that address through this node. Give a hostname and the panel resolves it, keeps the route in sync when the site moves, and refuses addresses that would hijack traffic inside the mesh.
Internet egress through gateways. Mark a server as an exit gateway and pick which devices may use it. In the Tailscale tray each user sees only the exits you allowed — the choice is theirs, the set is yours. A node's whole outbound traffic can also be forced through a gateway while the node stays reachable on its public IP.
An agent on the node. headscale can approve routes but has no channel to tell a
node what to advertise, so NodeRoost ships a small POSIX-sh agent: a systemd timer
pulls the desired state, applies it with tailscale set, and reports the hash of
what it actually applied.
Where a server sits. A country flag next to the node name, resolved from the node's public address offline against a table in the repository: no need to send every address in your fleet to a geo service for the sake of a flag, and no internet required. The node's name plays no part in it.
Backups, monitoring, alerts. Consistent snapshots of the headscale database and panel settings with a self-test and a rehearsed restore; uptime history; Telegram or webhook alerts for a downed server or an expiring key; and an independent host watchdog for when the panel itself goes quiet.
The panel runs a VPN network, so the guarantees live in code and are covered by tests:
- devices never reach each other, and a device is never a rule's destination — not by picking it from a list, not by typing its tailnet address by hand;
- a node cannot promote itself: its class and its tags follow only what the administrator approved, never what the node announces about itself;
- tags are owned by an empty group, so no node can apply one to itself;
- sign-in is JWT + optional TOTP with attempt limits and an audit log; the panel refuses to start with a weak secret or the default password;
- releases build from a hash-verified dependency lock, with base images pinned by digest.
Deployment notes that matter — network isolation, what is exposed publicly — are in SECURITY.md. Read it before putting this on the internet.
- Docker with Compose
- a reverse proxy terminating TLS (the compose overlay ships caddy-docker-proxy labels; give NodeRoost a network of its own — see SECURITY.md)
- two DNS names: one for the panel, one public one for the control server
Full guide: docs/install.md — one-command install, your own domain, the address allowlist, the firewall and what to do when something is wrong.
git clone https://github.com/mihsergeev/NodeRoost.git
cd NodeRoost
cp .env.example .envFill in .env — at minimum a random NODEROOST_JWT_SECRET (openssl rand -hex 32),
a strong NODEROOST_ADMIN_PASSWORD, NODEROOST_DB_PASSWORD, your two domains and
the addresses allowed to reach the panel. Then put the headscale config in place:
mkdir -p data/headscale/config
cp deploy/headscale/config.example.yaml data/headscale/config/config.yaml
$EDITOR data/headscale/config/config.yaml # server_url, base_domain, prefixes
docker compose up -dThe backend needs a headscale API key, which can only be created once headscale is running:
docker compose exec headscale headscale apikeys create --expiration 999d
# put it in .env as NODEROOST_HEADSCALE_API_KEY, then:
docker compose up -d backendSign in with the admin credentials from .env and add your first node — the panel
hands you a one-time key and a ready-made command for Linux, Windows, macOS or
Android.
┌───────── public ──────────┐ ┌──── allow-listed ────┐
nodes ───► │ hs.example.com │ │ panel.example.com │
│ headscale, node endpoints │ │ SPA + panel API │
│ only (/api/v1 → 404) │ └──────────┬───────────┘
└──────────┬────────────────┘ │
│ internal docker network │
└─────────► headscale API ◄────────┘
The panel reaches headscale's management API only over the internal network; on the
public vhost /api/v1 and /swagger return 404, leaving the node-facing endpoints.
Panel state lives in Postgres, headscale keeps its own SQLite.
cd backend && python -m venv .venv && .venv/bin/pip install -e . && pytest
cd frontend && npm install && npm run devThe country flag next to a node is resolved from its public address offline — none of your fleet's addresses leave the host. The table (backend/app/data/geoip.csv.gz, DB-IP IP-to-Country Lite, CC BY 4.0) ships in the repository; refresh it every few months:
python ops/build-geoip.pyBSD-3-Clause — see LICENSE.




