Headscale or NetBird? If you already like the Tailscale apps and want the smallest possible self-hosted control server, pick Headscale: it is one Go binary under the BSD-3-Clause licence, it ran in about 54 MB of RAM in our lab, and the official Tailscale clients connect to it. If you want a web dashboard, user logins and click-to-edit access policies out of the box, pick NetBird: its quickstart starts three Docker containers that used about 74 MiB together, but it wants a domain name, 2 GB of RAM and open ports 80, 443 and 3478/udp. Both build a WireGuard mesh VPN where your devices talk to each other directly, and both run well on a small VPS.
Key facts, checked on 9 October 2026:
- Headscale v0.29.4 is the current release, published on 23 September 2026 (Headscale releases). The project describes its scope as "a single Tailscale network (tailnet), suitable for a personal use, or a small open-source organisation" (headscale.net).
- NetBird v0.80.0 is the current stable release, published on 1 October 2026 (NetBird releases).
- NetBird's self-hosted quickstart asks for "at least 1 CPU and 2 GB of memory", a public domain name, TCP 80 and 443 and UDP 3478 (NetBird quickstart).
- On GitHub, Headscale has 44,457 stars and NetBird 29,818 (GitHub API, 9 October 2026).
- Headscale v0.29.0 changed what
*means in access rules: it now matches only the tailnet's own ranges (100.64.0.0/10 and fd7a:115c:a1e0::/48), and a source meaning "all IPs" is now writtenautogroup:danger-all(release notes). Old policy files can behave differently after an upgrade.
What a control server does, in plain words
A mesh VPN connects every device to every other device with an encrypted WireGuard tunnel. Somebody has to hand out addresses, swap public keys, tell each device where the others are, and decide who may talk to whom. That is the control server (also called the coordination or management server).
The control server does not carry your traffic: devices send packets straight to each other. When they cannot, for example both behind strict NAT, traffic goes through a relay (Tailscale calls these DERP servers).
Tailscale runs the control server for you as a cloud service, and our guide to Tailscale on a VPS covers that route, from exit nodes to subnet routers. This article is about running the control server yourself, so your device list, keys and rules live on a machine you control.
Headscale and NetBird side by side
| Headscale 0.29.4 | NetBird 0.80.0 | |
|---|---|---|
| What it is | An open-source rewrite of Tailscale's control server. Not affiliated with Tailscale Inc. | A complete stack from one company: clients, management, signal, relay and dashboard |
| Clients | The official Tailscale apps, pointed at your server with --login-server | NetBird's own apps, pointed at your server with --management-url |
| Licence | BSD-3-Clause | AGPLv3 for management, signal, relay and the combined server; BSD-3-Clause for the rest, including the client |
| Install | One .deb package, or a Docker image | A script that writes a Docker Compose file with 3 containers |
| Web interface | None built in. Community projects such as Headplane | Built-in dashboard |
| User logins | Pre-auth keys, CLI approval or any OIDC provider | Built-in user accounts (embedded Dex), or Google, Microsoft, Okta and other IdPs |
| Access rules | A HuJSON policy file in Tailscale's format | Groups and policies in the dashboard or REST API, plus posture checks |
| Exit nodes and subnet routes | Yes, approved on the CLI | Yes, as network routes |
| DNS | MagicDNS, split DNS, extra records | Peer names under a domain, custom nameservers per group |
| Relays | Uses Tailscale's public DERP servers by default; an embedded DERP server can be switched on | Relay and STUN run inside the NetBird server |
| Domain and TLS | Recommended for real use; plain HTTP works for a test | Required by the official quickstart, except an IP-only HTTP mode |
Feature lists come from the Headscale features page, the Headscale web UI list, NetBird's LICENSE file and our own lab.
How we tested both
We used one of our Amsterdam KVM servers running Debian 13 and gave each piece its own Debian 13 container: Headscale, two Tailscale clients, the NetBird server with Docker, and two NetBird clients. The 10.141.188.x addresses below are private lab addresses, so read them as "your server's IP". We had no public domain, which matters for NetBird.
Every command in this article was run in that lab on 9 October 2026, and every output shown is what we got. Installing the Tailscale client took about 5 seconds per machine and the NetBird client 6 to 7 seconds.
Headscale: install it and connect two Tailscale clients
Step 1: install the package
Headscale's docs recommend the .deb package on Debian 12 or newer and Ubuntu 22.04 or newer (install docs). The package creates a headscale system user and starts the service for you.
# as a normal user with sudo, on the Headscale server
HEADSCALE_VERSION="0.29.4"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt-get install -y ./headscale.deb
headscale version
systemctl is-active headscale
The download is 20 MB and the install took 1.5 seconds. You should see:
headscale version v0.29.4
active
Step 2: tell it its own address
Out of the box Headscale listens only on 127.0.0.1:8080. Three lines in /etc/headscale/config.yaml need changing: server_url (the address clients use), listen_addr, and base_domain (the DNS suffix for your devices, which must not be the same domain as the server URL).
# as a normal user with sudo, on the Headscale server
sudo sed -i 's|^server_url: .*|server_url: http://10.141.188.74:8080|; s|^listen_addr: .*|listen_addr: 0.0.0.0:8080|; s|^ base_domain: .*| base_domain: mesh.internal|' /etc/headscale/config.yaml
sudo systemctl restart headscale
sudo headscale users create alice
The last command answers User created, and sudo headscale users list shows alice with ID 1. On a public server, put Headscale behind a reverse proxy with a real certificate and set server_url to something like https://hs.example.com; our Caddy, Nginx and Traefik guide shows how. Plain HTTP on a private address is only for a test.
Step 3: join the first device with a pre-auth key
A pre-auth key lets a device join without anyone approving it. It is the easy way for servers.
# as root on the Headscale server
headscale preauthkeys create --user 1 --expiration 1h
It prints a key that starts with hskey-auth-. On the first client, install Tailscale with the official script (curl -fsSL https://tailscale.com/install.sh | sh, which installed 1.104.1 for us) and join:
# as root on the first client
tailscale up --login-server http://10.141.188.74:8080 --authkey hskey-auth-PASTE-YOUR-KEY --hostname laptop
tailscale ip -4
It took 2 seconds and answered 100.64.0.1.
Step 4: join the second device by approval
Without a key, the client prints a link and waits. Whoever runs Headscale approves it on the server, which replaces the "Approve" button of the Tailscale admin console.
# as root on the second client
tailscale up --login-server http://10.141.188.74:8080 --hostname vps-node
To authenticate, visit:
http://10.141.188.74:8080/register/hskey-authreq-LydIOF31oQ0y5lazvy6L8Z0c
Opening that page shows the exact command to run. We ran it with our user name:
# as root on the Headscale server
headscale auth register --auth-id hskey-authreq-LydIOF31oQ0y5lazvy6L8Z0c --user alice
It replied Node vps-node registered. Older guides use headscale nodes register; since v0.29.0 that command is deprecated in favour of headscale auth register.
Step 5: check that the two talk directly
# as root on the first client
tailscale ping -c 3 vps-node
ping -c 3 vps-node.mesh.internal
pong from vps-node (100.64.0.2) via 10.141.188.165:41641 in 1ms
3 packets transmitted, 3 received, 0% packet loss, time 2010ms
"via 10.141.188.165:41641" means a direct WireGuard path, no relay. The name vps-node.mesh.internal resolved through MagicDNS, Tailscale's built-in DNS for devices in the mesh, which Headscale serves too.
NetBird: the quickstart, and what happens without a domain
NetBird's server runs in Docker. We installed Docker with its official script and added our normal user to the docker group:
# as root on the NetBird server
apt-get install -y curl ca-certificates jq
curl -fsSL https://get.docker.com | sh
usermod -aG docker admin
Docker took 19 seconds to install. For more on running containers on a server, see our Docker on a VPS guide.
The normal path: a domain and Let's Encrypt
The quickstart is one script, getting-started.sh. On a real server you point a domain such as netbird.example.com at its public IP, and the official quickstart has you set three variables before running the script: NETBIRD_DOMAIN (your domain), NETBIRD_LETSENCRYPT_EMAIL (a real address for certificate notices) and NETBIRD_NON_INTERACTIVE=true. The bundled Traefik proxy then fetches a Let's Encrypt certificate for you. Follow the docs for that path; we could not test it, because a lab has no public domain.
The script refuses a bare IP address, but it has a special value for exactly our case: use-ip, which "installs on this host's IP over HTTP". That one we ran, with the same script and only the domain changed:
# as the admin user (member of the docker group) on the NetBird server
mkdir -p ~/netbird && cd ~/netbird
export NETBIRD_DOMAIN=use-ip
export NETBIRD_LETSENCRYPT_EMAIL=admin@example.org
export NETBIRD_NON_INTERACTIVE=true
curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh | bash
docker compose ps
It pulled the images and finished in 12.7 seconds with three containers:
NAME IMAGE STATUS
netbird-dashboard netbirdio/dashboard:latest Up 3 seconds
netbird-server netbirdio/netbird-server:latest Up 3 seconds
netbird-traefik traefik:v3.6 Up 3 seconds
netbird-server is the "combined server": management, signal, relay and STUN in one process. Older guides show separate containers plus a Zitadel or Keycloak login server; the embedded Dex login replaced them.
A snag we hit in IP mode
In v0.80.0 the IP mode wrote http:// addresses into its config but kept Traefik's redirect from HTTP to HTTPS, where Traefik could only offer a self-signed certificate (Let's Encrypt does not issue certificates for private IPs, and it also rejected our example.org contact address). The dashboard and clients could not work like that. For the lab we removed the redirect and TLS settings from the generated compose file:
# as the admin user, in ~/netbird (lab only: this turns off HTTPS)
cp docker-compose.yml docker-compose.yml.orig
sed -i -e '/redirections.entrypoint/d' -e '/tls.certresolver=letsencrypt/d' -e 's/entrypoints=websecure/entrypoints=web/' docker-compose.yml
sed -i "/routers.netbird-[a-z]*.tls=true/d" docker-compose.yml
docker compose up -d
After that, http://10.141.188.17/ answered with HTTP 200 and the dashboard loaded. Do not copy this to a public server. On the internet, use the domain path above so logins and keys travel over HTTPS.
First user, setup key and two clients
On the first visit the dashboard asks you to create the owner account. Then, under Setup Keys, you create a key for devices to join, much like Headscale's pre-auth key. We did both through the server's REST API, which takes the same input. The client install and join:
# as root on each NetBird client
curl -fsSL https://pkgs.netbird.io/install.sh | sh
netbird up --management-url http://10.141.188.17:80 --setup-key <SETUP-KEY>
netbird status
Connected
Management: Connected
Signal: Connected
Relays: 2/2 Available
FQDN: wf-hsnb-c3.netbird.selfhosted
NetBird IP: 100.69.155.142/16
Lazy connection: true
"Lazy connection" means peers only build a tunnel when traffic needs one. Our first ping test between the two clients lost 3 of 4 packets, and the one reply took 5.2 seconds while the tunnel was being built. After that the peers stayed connected. netbird status -d then showed Connection type: P2P with both ends on IPv6 addresses, so NetBird chose the IPv6 path on its own.
Access rules, exit nodes and DNS in practice
Access rules: a file against a dashboard
Both start with everyone allowed to reach everyone. We set the same rule in each: laptops may reach servers on SSH (TCP port 22), nothing else between them.
In Headscale the rules live in a policy file. We saved this as /etc/headscale/policy.hujson:
// as root: save as /etc/headscale/policy.hujson on the Headscale server
{
"tagOwners": {
"tag:server": ["alice@"]
},
"acls": [
// alice's devices may reach servers on SSH only
{ "action": "accept", "src": ["alice@"], "dst": ["tag:server:22"] },
// and may use an exit node to reach the internet
{ "action": "accept", "src": ["alice@"], "dst": ["autogroup:internet:*"] }
]
}
# as root on the Headscale server
sed -i 's|^ path: ""| path: /etc/headscale/policy.hujson|' /etc/headscale/config.yaml
headscale policy check --file /etc/headscale/policy.hujson
systemctl restart headscale
headscale nodes tag --identifier 2 --tags tag:server
The check answered Policy is valid. From the laptop, port 22 on vps-node connected and a test listener on port 8080 timed out, as intended. One detail surprised us: ping from laptop to server still worked. Tailscale clients appear to allow ping to any device you may reach on some port, so do not use ping to test a Headscale policy. Ping from the server back to the laptop failed.
In NetBird you do the same with two groups ("laptops" and "servers") and a policy under Access Control, with protocol TCP and port 22, after removing the "Default" policy that allows All to All. We did it through the API and got the same result for ports 22 and 8080. Ping was blocked in both directions, because NetBird filters ICMP like any other protocol. NetBird can also attach posture checks to a policy, such as a minimum client version or the country a device connects from; we did not test those.
Exit nodes
An exit node is a device that sends the internet traffic of other devices out through its own connection. A VPS in a data centre is the classic choice, for example to get a fixed public IP while you travel; the Tailscale exit node guide covers the client side in detail. With Headscale it takes three steps: enable forwarding on the exit node, advertise it, then approve the route on the server.
# as root on vps-node (the future exit node)
echo net.ipv4.ip_forward=1 > /etc/sysctl.d/99-tailscale.conf
echo net.ipv6.conf.all.forwarding=1 >> /etc/sysctl.d/99-tailscale.conf
sysctl -p /etc/sysctl.d/99-tailscale.conf
tailscale set --advertise-exit-node
# as root on the Headscale server
headscale nodes list-routes
headscale nodes approve-routes --identifier 2 --routes 0.0.0.0/0,::/0
# as root on the laptop
tailscale set --exit-node=vps-node
traceroute -n -m 3 1.1.1.1
The first hop was 100.64.0.2, the exit node. In NetBird an exit node is a network route for 0.0.0.0/0 with masquerading, given to a group; in the dashboard it is the "Add Exit Node" button on a peer (NetBird exit node docs). We created it through the API, and the laptop applied it automatically: netbird routes list showed Network: 0.0.0.0/0, ::/0 with Status: Selected, and traceroute's first hop was the exit node's NetBird IP.
DNS and relays
Headscale named devices under mesh.internal, NetBird under netbird.selfhosted in IP mode. Both resolved peer names with no extra work.
Headscale's default config still points clients at Tailscale's public relay list (https://controlplane.tailscale.com/derpmap/default), and its embedded DERP server is off. So when two devices cannot connect directly, their encrypted traffic is relayed by Tailscale's servers. If you want no third party at all, turn on the embedded DERP server in config.yaml, which needs TLS and UDP port 3478. NetBird's relay is part of its own server from the start, which is why netbird status showed "Relays: 2/2 Available" against our lab server.
RAM and disk: what we measured
Measured in our lab with ps, docker stats, free -m and docker system df, with two clients connected and idle:
| What | Headscale 0.29.4 | NetBird 0.80.0 |
|---|---|---|
| Server processes | headscale: 54 MB (RSS) | server 32.5 MiB, dashboard 20.8 MiB, Traefik 20.9 MiB: about 74 MiB |
| Whole server, "used" in free -m | 33 MB | 239 MB, including the Docker daemon |
| Disk | 51 MB binary, 140 KB database | 775 MB of images, 74 MB of volumes (66 MB of it a GeoLite2 location database) |
| Install time | 1.5 s for the package | 19 s for Docker, 12.7 s for the script |
| Client daemon | tailscaled: 47 MB (RSS) | netbird: 68 MB (RSS) |
Both are light. NetBird's 2 GB minimum is headroom for Docker, image pulls and growth, not what the services use at rest.
Licences, and why they might matter to you
Headscale is BSD-3-Clause: you may run it, change it and ship it inside a product with almost no conditions. The Tailscale clients it relies on still belong to Tailscale Inc., and not all of them are open source.
NetBird splits its repository. The management, signal, relay and combined server directories are AGPLv3; everything else, including the client, is BSD-3-Clause. For your own use the AGPL changes nothing. It matters if you modify the server and let other people use it over a network, for example as a service you sell, because the AGPL then asks you to publish your changes. NetBird also sells a commercial on-prem licence, which the installer mentions when it finishes. If you plan to build a business on either, ask a lawyer, not a blog.
NetBird also offers a hosted cloud with a free plan for up to 5 users and 100 machines (NetBird pricing, checked 9 October 2026).
Which one should you choose?
Choose Headscale when you or your family already use Tailscale apps, you are comfortable on the command line, and you want the least to maintain: one package, one config file, one SQLite database. It is also the right pick when a permissive licence matters. Plan to add a web UI such as Headplane (MIT licence, v0.7.1 from 28 August 2026) if somebody else will manage devices; we did not test it.
Choose NetBird when several people need to log in, an administrator who does not live in a terminal will manage access, or you want your own relay and posture checks without extra parts. Budget a domain name and a reverse proxy or the bundled Traefik for real HTTPS.
Choose neither when you only need your laptop to reach one server. Plain WireGuard on a VPS needs no control server at all.
For sizing: Headscale is comfortable on our VPS Nano (1 vCPU, 1 GB RAM, 20 GB NVMe). NetBird's documented minimum of 2 GB points to the VPS Micro (2 vCPU, 2 GB, 40 GB NVMe). Each server comes with its own IPv4 and IPv6 address, and in Amsterdam and Dublin a whole /48 of IPv6. Put the control server close to most of your users: Amsterdam for Western Europe, Dublin for Ireland and the UK, Prishtina for the Balkans. Keep an eye on it with Uptime Kuma, because when the control server is down, no new device can join.
Frequently asked questions
Is Headscale free to use?
Yes. Headscale is open source under the BSD-3-Clause licence and has no paid edition. You only pay for the server it runs on. In our lab version 0.29.4 used about 54 MB of RAM, so the smallest VPS is enough for a home or small team network.
Does self-hosted NetBird need a domain name?
The official quickstart asks for a public domain pointing at the server, so Traefik can get a Let's Encrypt certificate. The script also has a use-ip mode for plain HTTP on the server's IP, but in v0.80.0 we had to edit the generated compose file before it worked, and it sends logins without encryption. Use a domain for anything real.
Does my VPN traffic pass through the control server?
No. Headscale and NetBird only coordinate: they hand out addresses, keys and rules. Devices send WireGuard traffic straight to each other. Only when no direct path exists does traffic go through a relay, which is Tailscale's DERP network by default for Headscale and NetBird's own relay for NetBird.
How much RAM does a Headscale or NetBird server need?
In our lab with two clients, Headscale's process used 54 MB and NetBird's three containers about 74 MiB, with 239 MB used on the whole NetBird server including Docker. A 1 GB VPS is plenty for Headscale. NetBird's documentation asks for at least 2 GB of RAM and 1 CPU.
Is NetBird fully open source?
The code is public, but under two licences. The management, signal, relay and combined server code is AGPLv3, and the rest of the repository, including the client, is BSD-3-Clause. NetBird also sells a commercial on-prem licence and runs a hosted cloud with a free plan.
Can I use the Tailscale phone apps with Headscale?
Headscale's documentation has setup pages for Android and Apple devices, where you enter your own server address in the app instead of Tailscale's. We tested only the Linux client, version 1.104.1, which joined with the --login-server flag in 2 seconds.
Give it a weekend
Both install in minutes, so the quickest way to decide is to try both, as we did. A VPS Nano is enough for Headscale and a VPS Micro for NetBird, and you can upgrade to a bigger plan later from the client area with a short reboot. Pick a city on the plans page, or message us on Telegram if you would like us to quote the setup for you.