You Like Tailscale But You Don’t Trust Their Servers — Fair
Tailscale is, genuinely, one of the best pieces of networking software of the last decade. You install it, you’re done. Your laptop at a coffee shop can reach your home server’s private IP like they’re on the same LAN. MagicDNS, ACLs, subnet routes, exit nodes — it just works. There’s a reason engineers who’ve hand-rolled WireGuard configs say “oh, this is what it was supposed to feel like.”
The catch is buried in the architecture. Every Tailscale node registers with a coordination server. That server stores your network map — who exists, what keys they have, what ACLs apply. Tailscale’s SaaS control plane is closed source. Your devices’ metadata, network topology, and node relationships live in someone else’s database. The free tier caps you at 3 users. And if you work in a context where “we use a third-party coordination server for our internal network” makes the security team reach for antacids, you’ve hit the wall.
Enter Headscale: the open-source reimplementation of the Tailscale control plane. You run it. Your devices still use the official Tailscale client — just pointed at your URL instead of login.tailscale.com. Same WireGuard magic. Same mesh. Your data.
Let’s deploy it.
What Headscale Actually Does (And Doesn’t Do)
Headscale replaces the coordination server only. The data plane — the encrypted WireGuard tunnels between your nodes — never changes. Your laptop still talks directly to your server using the same DERP relay infrastructure Tailscale built. Headscale doesn’t proxy your traffic, it just manages the key exchange and network map.
What you get:
- Unlimited users and devices (you’re hosting it, you make the rules)
- Full ACL control via a JSON policy file you own
- MagicDNS for
machinename.yourdomain.ts.net-style hostnames - Subnet routing and exit node support
- OIDC login (Authelia, Authentik, Keycloak — whatever you’re already running)
- No telemetry back to anyone
What you don’t get (yet):
- The Tailscale web admin UI (you use
headscaleCLI or the API) - Taildrive (file sharing feature)
- Some newer Tailscale features that haven’t been reverse-engineered yet
For 90% of self-hosting use cases: doesn’t matter.
Prerequisites
- A server with a public IP and ports 80/443 available (VPS, homelab with port-forwarding — anything reachable from the internet)
- Docker + Docker Compose
- A domain you control (we’ll use
headscale.example.com) - Tailscale client installed on the machines you want to connect
Headscale needs to be reachable by your nodes. If you’re behind CGNAT at home, a cheap VPS ($4/month) is worth it — run Headscale there, all your nodes phone home to it.
Deploying Headscale with Docker Compose
Create a directory for the deployment and the config:
mkdir -p /opt/headscale/{config,data}cd /opt/headscaleThe config file is where most of the setup lives. Create a minimal working one:
server_url: https://headscale.example.comlisten_addr: 0.0.0.0:8080metrics_listen_addr: 127.0.0.1:9090grpc_listen_addr: 0.0.0.0:50443grpc_allow_insecure: false
private_key_path: /var/lib/headscale/private.keynoise: private_key_path: /var/lib/headscale/noise_private.key
ip_prefixes: - fd7a:115c:a1e0::/48 - 100.64.0.0/10
derp: server: enabled: false urls: - https://controlplane.tailscale.com/derpmap/default auto_update_enabled: true update_frequency: 24h
disable_check_updates: trueephemeral_node_inactivity_timeout: 30m
database: type: sqlite sqlite: path: /var/lib/headscale/db.sqlite
acls_policy_path: /etc/headscale/acl.json
dns: magic_dns: true base_domain: yourdomain.ts.net nameservers: global: - 1.1.1.1 - 8.8.8.8 search_domains: [] extra_records: []
log: level: info
tls_letsencrypt_hostname: headscale.example.comtls_letsencrypt_cache_dir: /var/lib/headscale/cachetls_letsencrypt_challenge_type: HTTP-01tls_letsencrypt_listen: ":http"A few things worth pointing out: base_domain becomes the suffix for your MagicDNS hostnames. It doesn’t have to be a real domain you own — Headscale uses it internally. ip_prefixes controls the IP ranges assigned to nodes; the defaults are fine. We’re using SQLite because unless you’re running a serious multi-admin shop, it’s more than adequate.
Now the Compose file:
services: headscale: image: headscale/headscale:latest container_name: headscale restart: unless-stopped ports: - "80:8080" - "443:8080" - "9090:9090" volumes: - ./config:/etc/headscale - ./data:/var/lib/headscale command: serve
headscale-ui: image: goodieshq/headscale-admin:latest container_name: headscale-ui restart: unless-stopped ports: - "8081:80" environment: - HEADSCALE_URL=https://headscale.example.comThe headscale-admin image is a community-built web UI. It’s not official, but it’s genuinely useful for visualizing your nodes without memorizing CLI commands. Don’t expose port 8081 publicly — reverse proxy it behind auth if you want it accessible remotely.
Note on TLS: The config above uses Let’s Encrypt via HTTP-01 challenge, which means Headscale needs to answer on port 80 directly. If you’re already running a reverse proxy (Caddy, Nginx, Traefik), you’ll want to either let Headscale handle TLS itself (works cleanly if it owns those ports) or configure
tls_cert_path/tls_key_pathand have your proxy terminate TLS and forward to port 8080. Both work. The Let’s Encrypt path is the least-config option if you have a clean server.
Fire it up:
docker compose up -ddocker compose logs -f headscaleYou should see Headscale start, attempt ACME cert provisioning, and begin listening. Give it 30 seconds. Check https://headscale.example.com/health — you want {"status":"pass"}.
Creating Your First User and Registering a Node
Headscale organizes devices into “users” (previously called namespaces). Think of them as logical groupings — you’d create one per person, or per environment if you’re organizing by role.
# Create a userdocker exec headscale headscale users create alice
# List usersdocker exec headscale headscale users listNow on the machine you want to connect — your laptop, say — run the Tailscale client but point it at your Headscale instance:
sudo tailscale up \ --login-server=https://headscale.example.com \ --accept-routes \ --accept-dnsTailscale will print a URL: https://headscale.example.com/register/nodekey:...
Copy that URL, then on your server:
# Register the node to the alice userdocker exec headscale headscale nodes register \ --user alice \ --key nodekey:PASTE_THE_KEY_HEREThat’s it. The node is registered. Check it:
docker exec headscale headscale nodes listRepeat for every machine. Add a second node, register it to the same user or a different one, and they’ll be able to reach each other via their Tailscale IPs.
ACLs: Defining Who Can Talk to What
Without an ACL file, all nodes can reach all other nodes. That’s fine for a two-device personal setup. For anything more structured, you want rules.
Create the policy file your config.yaml is pointing at:
{ "groups": { "group:admins": ["alice"], "group:homelab": ["alice", "bob"] }, "hosts": { "nas": "100.64.0.3", "pihole": "100.64.0.4" }, "acls": [ { "action": "accept", "src": ["group:admins"], "dst": ["*:*"] }, { "action": "accept", "src": ["group:homelab"], "dst": ["nas:443,22", "pihole:53"] } ]}Admins can reach everything. The homelab group can hit the NAS on HTTPS and SSH, and the Pi-hole on DNS. Everyone else gets nothing by default. This is the same HuJSON-ish ACL syntax Tailscale uses — most examples you find for Tailscale ACLs will work with minor formatting adjustments.
Reload the policy without restarting:
docker exec headscale headscale policy set --file /etc/headscale/acl.jsonExit Nodes and Subnet Routes
Exit node — route all traffic from a node through another node. Useful when you want one machine (your home server, a VPS) to act as a VPN exit point.
On the machine that will be the exit node:
sudo tailscale up \ --login-server=https://headscale.example.com \ --advertise-exit-nodeOn the Headscale server, approve it:
docker exec headscale headscale routes listdocker exec headscale headscale routes enable --route <ROUTE_ID>On any client that wants to use it:
sudo tailscale up \ --login-server=https://headscale.example.com \ --exit-node=<EXIT_NODE_IP>Subnet routes work the same way but instead of routing all traffic, you expose a LAN segment. Say your home server is at 192.168.1.5 and you want your phone to reach your whole home network:
# On the home serversudo tailscale up \ --login-server=https://headscale.example.com \ --advertise-routes=192.168.1.0/24Approve it on Headscale:
docker exec headscale headscale routes enable --route <ROUTE_ID>Now your phone — registered to Headscale but sitting anywhere on the internet — can reach 192.168.1.x addresses directly. This is the killer feature for home lab access. No port-forwarding pyramid of doom, no dynamic DNS hacks. Just “your whole LAN, accessible from anywhere.”
MagicDNS
You set magic_dns: true in the config. Now every node gets a hostname: machinename.yourdomain.ts.net. To reach your NAS from your laptop, you can do ssh nas.yourdomain.ts.net instead of remembering Tailscale IPs.
The Tailscale client handles the DNS resolution automatically when it registers. Make sure your nodes have --accept-dns set in their tailscale up command (it’s shown in the examples above).
To add custom DNS records — say, you want homeassistant.yourdomain.ts.net to resolve to a specific IP — use extra_records in the config:
dns: extra_records: - name: homeassistant.yourdomain.ts.net type: A value: 192.168.1.50Reload Headscale after config changes:
docker compose restart headscaleOIDC: Let Your SSO Handle Logins
If you’re already running Authentik, Authelia, Keycloak, or any OIDC provider, you can hook it up so users authenticate through SSO instead of manual headscale nodes register commands. They browse to your Headscale URL, hit your SSO, and their node gets auto-registered.
Add to config.yaml:
oidc: only_start_if_oidc_is_available: true issuer: https://auth.example.com/application/o/headscale/ client_id: headscale client_secret: your-client-secret scope: - openid - profile - email strip_email_domain: trueUsers then run:
sudo tailscale up --login-server=https://headscale.example.comTailscale opens a browser URL, they log in through your SSO, node is registered and assigned to a user matching their email. Slick. Especially useful when you’re onboarding family members who would never remember a registration command.
Headscale vs. Tailscale: The Honest Take
Tailscale’s free tier covers 1 admin, up to 3 users, and 100 devices. For most single-person homelabs, that’s actually fine. You get the official polished experience, automatic updates to new features, and you don’t need to maintain a public-facing server.
Use Headscale when:
- You need more than 3 admin users or want to share admin access without paying for Tailscale’s team plan
- You’re working in an air-gapped or highly regulated environment where network metadata must stay internal
- You want complete ownership of who can see your network topology
- You’re already maintaining a VPS or public server for other reasons — the operational overhead is minimal
- You’re self-hosting everything else and Tailscale’s SaaS feels philosophically inconsistent
Stick with Tailscale when:
- It’s just you and maybe one other person — the free tier genuinely covers this
- You want zero maintenance (Tailscale handles everything, including DERP infrastructure)
- You’re relying on newer features like Taildrive that Headscale hasn’t implemented
- You don’t have a server with a public IP to run Headscale on
- You’re already paying for Tailscale’s personal plan and getting value from it
The deployment we walked through here — Docker Compose, Let’s Encrypt, SQLite — will run happily on a $4 VPS and consume maybe 30 MB of RAM. Maintenance is occasional docker compose pull && docker compose up -d when new releases drop. It’s not a burden.
What you’re trading for that simplicity is Tailscale’s DERP relay network and the upstream polish. Headscale uses Tailscale’s own DERP servers by default (this is allowed by their terms for non-commercial self-hosted use), so connectivity is actually excellent — your traffic still benefits from their relay infrastructure, you’re just using a different coordination server.
The real question isn’t technical. It’s philosophical. Do you care who holds the keys to your network map? If yes, Headscale is your answer. If the SaaS convenience outweighs the data sovereignty concern, Tailscale’s free tier is genuinely excellent software.
Either way, the WireGuard mesh is waiting. Stop forwarding ports.