Head to head
Headscale vs wg-easy
If you can forward a UDP port, wg-easy is the smaller and more comprehensible answer. If you cannot, that is not a preference any more, and the mesh is the only thing that works.
Should you run Headscale or wg-easy for remote access?
Run wg-easy if you can forward one UDP port to your server, because a hub-and-spoke WireGuard tunnel is fewer moving parts and you can hold all of it in your head. Run Headscale if you are behind CGNAT or a NAT you do not control, since it gives official Tailscale clients NAT traversal, MagicDNS and ACLs without your device inventory living at Tailscale Inc. Headscale is not affiliated with or supported by Tailscale, is pre-1.0 with breaking changes between minors, and tracks a minimum client version, currently v1.80.0.
One question settles most of this: can you forward a UDP port from the internet to your server?
If yes, wg-easy is a WireGuard server with a QR code generator attached, and that is the right amount of software for getting your phone and laptop back onto your home LAN. Every packet goes to one endpoint you chose, the topology fits on a napkin, and when it breaks you can reason about it.
If no, because your ISP has you behind carrier-grade NAT or you are in a flat with a router you do not administer, the question stops being a preference. Plain WireGuard needs a reachable endpoint. Headscale gives official Tailscale clients NAT traversal, MagicDNS and ACLs, with the coordination server on your hardware instead of at Tailscale Inc.
Everything else on this page is detail on those two paths, plus a third one you should consider seriously: not self-hosting the control plane at all.
What the table does and does not tell you#
Both are one container. The rows worth your attention are ops load and maturity, because they encode the thing that actually differs: Headscale is pre-1.0 software chasing a protocol it does not own.
| Specification | Headscale | wg-easy |
|---|---|---|
| Licence | BSD-3-Clause (Permissive) | AGPL-3.0-only (Copyleft) |
| Written in | Go | TypeScript / Node.js (Nuxt) |
| First release | 2021 | 2021 |
| Maturity | Young | Stable |
| Datastore | SQLite (recommended); PostgreSQL supported but in maintenance mode | SQLite since v15; v14 and earlier used a JSON file |
| Services to run | 1 | 1 |
| Idle memory | 40 MB | 90 MB |
| Memory in use | 120 MB | 160 MB |
| Operational load | 3 / 5, Moderate | 2 / 5, Light |
| Identity | Native OIDC | Native OIDC |
| arm64 builds | Yes | Yes |
| Default ports | 8080, 9090, 3478 | 51820, 51821 |
| Backup shape | SQLite backup | SQLite backup |
Headscale's memory looks smaller, but the data plane is the difference that matters. In Headscale, traffic goes peer to peer and only falls back to a DERP relay when direct connection fails, so the server sees coordination traffic and nothing else. In wg-easy, every packet from every client crosses your server, because that is what hub-and-spoke means.
Hub and spoke against a mesh#
A wg-easy deployment is one WireGuard interface on one host, and every peer's config points at it:
[Interface]
PrivateKey = <client key>
Address = 10.8.0.3/24
[Peer]
PublicKey = <server key>
Endpoint = vpn.example.com:51820
AllowedIPs = 192.168.1.0/24
PersistentKeepalive = 25Your phone dials the server. Your laptop dials the server. If your phone and your laptop want to talk to each other, the traffic goes through the server. If the server is unreachable, nothing works. That simplicity is the feature.
Headscale coordinates a mesh. Each device runs the official Tailscale client, joins with tailscale up --login-server=https://hs.example.com, receives a network map, and then attempts direct connections to peers using UDP hole punching, falling back to a DERP relay when NAT is uncooperative. Every device gets a stable 100.64.x.x address and a MagicDNS name. Nodes can reach each other whether or not the coordination server is up, once they have a map.
That is genuine capability. It is also more machinery: an ACL policy, a base domain, node approval, subnet route enabling, pre-auth keys, and a version relationship with every client. Both want TLS terminated in front of their web side, so Caddy or an equivalent is a prerequisite either way.
The Headscale costs, stated up front#
- It is not affiliated with or supported by Tailscale. The README says so. One maintainer works there and contributes during work hours, which is a good sign for the project and not a support contract.
- The minimum client version moves. v0.29.3 documents a floor of Tailscale client v1.80.0. Distro packages lag, and when they lag the symptom is a registration that hangs or a node that never gets a working network map. Use Tailscale's own repositories on machines you care about.
- You cannot skip minor versions. Upgrade one minor at a time, in order. 0.28 removed migrations for databases older than 0.25.0, so an instance left alone for a year cannot jump to current.
- There is no web UI. Users, pre-auth keys, node approval and route enabling are
headscalesubcommands or gRPC. Third-party UIs exist, of varying quality. base_domainmust differ from the domain inserver_url, or it refuses to start rather than build a resolution loop. Useserver_url: https://headscale.example.comwithbase_domain: ts.example.net.- 0.29 redefined the ACL wildcard. A bare
*now resolves to the Tailscale CGNAT range plus the IPv6 ULA range, not every address. Rules that granted access to LAN subnets or internet destinations through an exit node stop working after the upgrade, and the symptom is a healthy tailnet with newly broken subnet routing.
The maintainers are also blunt about scale: hundreds of nodes, not thousands, SQLite rather than PostgreSQL (which is in maintenance mode), and maintainability prioritized over performance. For a homelab that is the right set of tradeoffs, stated honestly.
The wg-easy costs, stated up front#
- v15 is a different product from v14. A full rewrite with, in the maintainer's words, no migration path at all. You export from v14, deploy v15 into a clean volume, and import during the setup wizard. Import is not available once setup completes, so clicking past it means starting over.
- The
latesttag still points at v14. Pin:15. - The licence changed to AGPL-3.0-only in v15, from CC BY-NC-SA 4.0. Commercial internal use is now unambiguously fine; the network copyleft clause applies if you modify it and offer it as a service. Either way, the terms you evaluated in 2024 are not the terms you get today.
- 32-bit ARM images are gone. armv6 dropped in 15.0, armv7 in 15.2.0. A Pi 3 or Zero on a 32-bit OS cannot pull v15 and there is no v14 fallback that shares a config format.
- It needs real privileges.
NET_ADMINandSYS_MODULE, the sysctlsnet.ipv4.ip_forward=1andnet.ipv4.conf.all.src_valid_mark=1, and a read-only bind of/lib/modulesso it can load the kernel module.
Headscale's equivalent is /var/lib/headscale: db.sqlite plus noise_private.key, which is the server's identity. Lose the key and every node re-registers from scratch. Neither is large; both need sqlite3 db.sqlite ".backup out.db" rather than a live cp, and Backups that actually restore covers automating that.
The case for just using hosted Tailscale#
This is the uncomfortable section, so here it is plainly: for most people reading this, Tailscale's hosted control plane is the correct answer, and Headscale is a preference rather than a requirement.
Self-hosting the coordination server buys you exactly one thing: your device inventory, the key exchange coordination and your ACL policy stay on hardware you own. That is a real privacy gain and for some people a real compliance one.
What it does not buy you is independence. You are still running Tailscale's client, speaking Tailscale's protocol, following Tailscale's version cadence, and by default relying on DERP relays you do not run unless you enable the embedded one and open udp/3478 for STUN. You have taken on an upgrade treadmill, a client-version matrix and a pre-1.0 database migration path, and you are still inside somebody else's ecosystem.
If you can state why the device list must not live at Tailscale, run Headscale. If you cannot, use the hosted free tier and spend the saved evenings on something else. wg-easy remains the answer when you want no ecosystem at all: one UDP port, one config file format, no vendor.
Which one for your situation#
| Situation | Use | Why |
|---|---|---|
| You can forward a UDP port, and want your phone on the LAN | wg-easy | One endpoint, a QR code, done |
| CGNAT with no forwardable port | Headscale | NAT traversal is the entire reason it exists |
| Devices in several locations that must reach each other | Headscale | Mesh, not hub and spoke |
| A family member needs a VPN config and no explanation | wg-easy | Scan the QR code and it works |
| You want OIDC-gated device enrollment | Headscale | Point it at authentik or Pocket ID |
| Raspberry Pi 3 or Zero on a 32-bit OS | Headscale | wg-easy v15 publishes no 32-bit ARM images |
| You want a web UI to manage the VPN | wg-easy | Headscale has no official UI at all |
| You cannot articulate why the control plane must be yours | Hosted Tailscale | Same clients, none of the upgrade burden |
| Full-tunnel VPN for privacy on public wifi | wg-easy | Hub and spoke is what a full tunnel is |
What to do next#
Check whether you actually have a forwardable port before anything else: look at your router's WAN address and compare it to what a "what is my IP" page reports. If they differ, you are behind CGNAT and the decision is made. Remote access without port forwarding covers the alternatives including reverse tunnels, and if you go the mesh route, decide early whether DNS for your internal names resolves over the tunnel, which is where DNS for self-hosters and your AdGuard Home or Pi-hole instance come into it. Whichever you choose, put Uptime Kuma on the far side of the tunnel so you find out it is down before you are away from home, and read A security baseline for a home server before you publish anything on a port. The rest of the category is at Networking.
Questions#
Is Headscale supported by Tailscale?
No. The README states the project is not associated with Tailscale Inc, although one maintainer is employed there and contributes during work hours. The practical consequence is that the control protocol belongs to Tailscale and moves, and Headscale follows. A client auto-update can land a version Headscale has not caught up with, so pin or delay client updates on machines you cannot physically reach, and read Headscale release notes before letting a fleet upgrade itself.
Does wg-easy work behind CGNAT?
Not on the server side. Plain WireGuard needs one reachable endpoint, so if your ISP puts you behind carrier-grade NAT and gives you no forwardable port, there is nothing to dial. Clients behind CGNAT are fine as long as the server is reachable. If both ends are unreachable, that is a NAT traversal problem, which is what a mesh VPN with relay servers exists to solve. Put wg-easy on a cheap VPS with a public IP, or use Headscale or Tailscale.
What Tailscale client version does Headscale need?
v0.29.3 (July 2026) documents a floor of Tailscale client v1.80.0, and that floor moves. Distro-packaged tailscaled on a long-term-support release is frequently below it, and the failure mode is a registration that hangs or a node that connects and never receives a working network map. Check client versions across your fleet before upgrading the server, and prefer Tailscale's own repositories over the distribution package on machines that stay online for years.
Can I upgrade Headscale straight to the latest version?
No. You have to step through each stable minor in order, for example 0.26.0 to 0.27.1 to 0.28.0 to 0.29.x. 0.28 removed database migrations for anything older than 0.25.0, so an instance left on an old release cannot jump: it starts, fails the migration, and you are restoring the backup. Pull each intermediate image, start it once, confirm it comes up, then continue.
Why does my wg-easy container still look like the old version?
Because ghcr.io/wg-easy/wg-easy:latest still points at v14, and the docs tell you to avoid that tag. Anyone who copied a compose file from a blog post in the last few years is running v14 without knowing it, on the old CC BY-NC-SA licence and without the v15 security work. Pin :15. There is no upgrade path from v14: you export the configuration, deploy v15 into a clean volume, and import during the setup wizard, which is not available once setup completes.
Should I just use Tailscale's hosted control plane?
For most people, yes, and it is worth saying plainly. The hosted service is free for personal use, the coordination server is somebody else's problem, DERP relays are operated for you, and there is no minimum-client-version dance. Self-hosting the control plane buys you one thing: your device inventory, keys coordination and ACL policy stay on your hardware. If that is not a requirement you can articulate, you are taking on an upgrade burden for a principle.
Do I need a reverse proxy in front of either?
For Headscale, yes: it listens on 127.0.0.1:8080 by default and clients require HTTPS, with server_url matching exactly the name they dial. For wg-easy, effectively yes: v15 refuses to serve its admin interface over plain HTTP unless you set INSECURE=true, and the documented happy path is TLS terminated in front of port 51821. The WireGuard data path on 51820/udp does not go through the proxy either way.
Sources#
- Headscale repository README, licence and relationship to Tailscale Inc
- Headscale FAQ, scaling and database recommendation
- Headscale documentation, requirements and ports
- Headscale releases and breaking change notes
- wg-easy v15.0.0 release notes, rewrite and licence change
- wg-easy v15.0.0 discussion, migration and setup flow
- wg-easy documentation, tags and requirements
- wg-easy shipped docker-compose.yml, ports and sysctls
- wg-easy licence file
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.