Self-Hosting My Tailnet with Headscale

Mauricio TedeschiMauricio Tedeschi/Sep 01, 2026

For the last couple of years, Tailscale has been the quiet infrastructure underneath my personal life. My Linux homelab, my MacBook, and my Surface tablet all lived on the same tailnet, which meant I could SSH into the server from a coffee shop, open Immich on the couch, browse files, and pull up my homepage dashboard without opening a dozen ports on my router. It felt invisible in the best way — the kind of tool you stop thinking about because it just works.

Then a friend mentioned WireGuard, and I went down a rabbit hole that ended somewhere more interesting than a raw VPN config. I already had Tailscale. I liked Tailscale. What I did not love, once I sat with it, was the idea that my entire private mesh — the thing my homelab, laptop, and tablet all depended on — lived on someone else's control plane forever. Headscale is an open-source reimplementation of Tailscale's coordination server. Same clients. Same WireGuard tunnels. But the brain of the network moves home.

Same clients, different control plane

That distinction mattered more than I expected. Headscale is not a different protocol and it is not a performance upgrade. It is a question of ownership. Who runs enrollment? Who decides which devices get keys? What domain do your machines live under? With Tailscale Cloud, the answers are mostly "Tailscale Inc." With Headscale on my homelab, they are mine.

I kept the ergonomics I already liked. I did not rewrite WireGuard configs by hand or give up the Tailscale client on Windows. On macOS I ended up on the Homebrew CLI and daemon instead of the menu-bar app, but that came later. My tailnet names now live under a subdomain on a domain I own instead of a random *.ts.net suffix, which sounds like a small thing until you start typing short hostnames every day and realize you want the network to feel like yours. The homelab was already the center of gravity for my services — Immich, Homepage, Crafty, a file browser, a Zotero site, the usual rabbit hole of self-hosted software. Running Headscale there made that literal. The server that hosts my photos also hosts the control plane that lets me reach them from anywhere.

Self-hosted tailnet on the homelab

The migration itself was a single afternoon that felt longer than it had any right to be. I pointed a Headscale hostname at my home IP, forwarded ports 80 and 443 on my AT&T router, and stood up Headscale and Caddy in Docker on the homelab server. That part was almost boring. The interesting failures were human and environmental.

I learned not to run tailscale logout on my only admin laptop before the new control plane was proven. That is the kind of lesson you only need to learn once, usually at the worst possible moment. AT&T's router does not hairpin NAT properly, so devices on my home LAN could not always reach the Headscale hostname through the public IP during enrollment. A temporary hosts-file entry got me through that afternoon. Small details, large consequences — but that was migration day. The mess that actually sent me spiraling came a few days later, and it had almost nothing to do with Headscale.

I saw the Tailscale icon in my Mac dock, decided I did not want it there, and quit the app. When I opened it again, it said I was logged out. My laptop fell off the tailnet. tailscale up hung. tailscale logout timed out. I spent an evening chasing the wrong problems — hairpin NAT, /etc/hosts, /etc/resolver, dnsmasq overrides on the homelab — because the symptoms looked like DNS and enrollment failures. The real issue was simpler and dumber: I had two Tailscales on the same Mac. The menu-bar app and Homebrew's tailscaled were fighting over the same network extension. Quitting the GUI app did not stop the daemon cleanly; it left everything in a broken half-state where neither path could enroll or recover.

The fix was to pick one. I deleted the app, started the Homebrew daemon with sudo brew services start tailscale, and enrolled from the CLI. No dock icon, no network extension drama, no LAN-specific DNS hacks on the client. Just tailscale up against my Headscale server and I was back on the tailnet. All those resolver files and hosts entries I had added during debugging turned out to be unnecessary once the conflicting clients were gone.

Migration flow

During migration, there was another failure that looked green until it was not. Headscale said the devices were online. MagicDNS resolved. SSH worked. I opened my homepage dashboard and half my apps were dead. The culprit was almost insultingly mundane: several Docker services were still bound to my old Tailscale Cloud IP, an address that no longer existed on the new tailnet. Zotero, File Browser, Crafty, the homepage widgets — all quietly listening on a ghost interface. Nothing crashed. Nothing screamed. The services just were not there anymore. Grep saved me. Rebind the ports, fix host.docker.internal, update the stale *.ts.net links, recreate the containers. An hour of confusion compressed into ten minutes of mechanical cleanup once I knew what to look for.

That is the real shape of this project, I think. Headscale is the control plane, not the data plane. The VPN does not absolve you of listening on the right interfaces or keeping your configs honest. It gives you a private network you control — which is exactly what I wanted — but it does not make homelab administration disappear.

On a normal day now, the experience feels almost identical to Tailscale Cloud, and that is the point. I SSH into the homelab from my Mac without thinking about it — short MagicDNS names, nothing LAN-specific, nothing in /etc/hosts. My homepage comes up. Immich, Beszel, Crafty, the file browser — all reachable the same way. There is a single place where I decide who joins the network. If I want to share a service with a friend later, I can issue a key and set policy myself instead of hoping a vendor's sharing model still fits.

The payoff is not a flashy new feature. It is quieter than that. My private network's coordination server lives on the same machine that already runs the things I care about, under a domain I control, with a migration story I actually understand. I did not leave Tailscale so much as bring the part that mattered home.

If you are already running a homelab and living on Tailscale, Headscale is a reasonable weekend project — as long as you enroll your admin machine last, keep SSH access to the server before you touch your laptop, and grep for old tailnet IPs before you declare victory. On macOS, pick the app or the Homebrew CLI and daemon, not both. Quitting the dock icon is not the same as stopping Tailscale when you have two of them installed. The technology is not the hard part. The hard part is respecting that your network is infrastructure, even when it feels like magic.