Self-Host Headscale: Run Your Own Tailscale Control Server

Self-Host Headscale: Run Your Own Tailscale Control Server

Tailscale is amazing, but it relies on their cloud coordination server. Headscale lets you own that piece too. Here's how I run my own mesh VPN.

đź’ˇ Disclosure: This article contains affiliate links. If you make a purchase through these links, we may earn a small commission at no extra cost to you. This helps support the site and keeps the content free.

The One Thing That Bugged Me About Tailscale

I love Tailscale. It’s the best thing that happened to self-hosted networking since WireGuard itself. Zero config, NAT traversal that actually works, and it turns my scattered VPS boxes into one cozy little LAN.

But there’s one thing that always scratched the back of my mind: the coordination server.

Every time my machines talk to each other, they check in with Tailscale’s cloud. Who’s online? What’s their IP? Here’s the encryption key. It’s all handled transparently, but it means every device in my tailnet must reach control.tailscale.com to work.

What happens if Tailscale goes down? What if they change their pricing? What if I want to run this for a client who doesn’t want their network metadata touching US servers?

That’s where Headscale comes in.

Headscale is an open-source implementation of the Tailscale control server. Same protocol, same WireGuard magic underneath — but you run the coordination layer. Your devices check in with your server, not Tailscale’s.

I’ve been running it for six months. It’s been rock solid. Here’s everything I learned.

What Headscale Actually Does

Let’s get the terminology straight.

Tailscale is two things:

  1. The client (tailscaled daemon running on your machines)
  2. The control server (tailscale.com — the coordination layer)

The client generates WireGuard keys, negotiates NAT traversal, and sets up peer-to-peer connections. But it needs a coordination server to know who to connect to and how.

Headscale replaces that coordination server. Your clients still use the official Tailscale client — no fork, no custom build — they just point to your Headscale instance instead of tailscale.com.

The result: same mesh VPN, same ease of use, same WireGuard performance. But now the control plane lives on your hardware.

Why Bother?

I asked myself the same question when I first heard about Headscale. Tailscale is free for personal use (up to 3 users, 100 devices). Why add complexity?

Three reasons convinced me:

1. Offline resilience. If my internet goes down but my LAN is still up, devices on my home network can still communicate via Tailscale — because they already have the WireGuard keys cached. But new devices can’t join, and if the cache expires… you’re stuck. With Headscale, the coordination server is on my LAN. Everything keeps working.

2. No vendor lock-in. Tailscale is built on amazing tech, but it’s a VC-backed startup. Pricing changes, acquisition, or service changes are all possible. I’ve been burned by “free tiers” before (looking at you, Google Photos). Headscale means my mesh VPN survives any Tailscale corporate decision.

3. Data sovereignty. Your tailnet metadata — which devices are connected, their IPs, their names, their ACLs — lives on Tailscale’s servers by default. For homelab use, that’s fine. For a business, that’s a compliance nightmare. Headscale fixes that.

The trade-off? You need to maintain the Headscale server. Updates, backups, availability. It’s not hard, but it’s your problem now.

🚀NordVPN

Secure your server with a reliable VPN.

Get NordVPN →

Affiliate link — we may earn a commission at no extra cost to you.

What You’ll Need

Headscale is lightweight. Here’s what I’m running it on:

  • A VPS or home server with Docker. I use a $6/month Hetzner CX22 (2 vCPU, 4GB RAM). Headscale uses ~100MB RAM and negligible CPU.
  • A domain name (or a subdomain). You’ll need DNS for the Headscale endpoint.
  • Open ports: TCP 443 (HTTPS) and optionally UDP 3478 (STUN for NAT traversal).
  • Basic Linux + Docker skills. If you’ve deployed anything on this blog, you’re qualified.

The domain requirement is the real gotcha. Headscale needs a valid SSL certificate (we’ll use Let’s Encrypt), and that requires a domain. You can technically use the IP directly, but clients will throw certificate warnings and mobile apps may refuse to connect.

Installation with Docker Compose

Let’s set it up. I’m using a reverse proxy (Traefik) for SSL, but I’ll also show you the standalone approach with Caddy — it’s simpler for most people.

Directory Structure

mkdir -p ~/headscale && cd ~/headscale

Docker Compose (with Caddy for SSL)

This is the simplest setup. Caddy automatically handles Let’s Encrypt certificates.

# docker-compose.yml
services:
  headscale:
    image: headscale/headscale:latest
    container_name: headscale
    restart: unless-stopped
    command: headscale serve
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    environment:
      - TZ=Europe/Paris
    ports:
      - "127.0.0.1:8080:8080"
    networks:
      - headscale-net

  caddy:
    image: caddy:latest
    container_name: caddy-headscale
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    networks:
      - headscale-net
    depends_on:
      - headscale

networks:
  headscale-net:
    driver: bridge

volumes:
  caddy_data:

Caddyfile

The Caddyfile is refreshingly simple:

headscale.yourdomain.com {
    reverse_proxy headscale:8080
}

Replace headscale.yourdomain.com with your actual domain. Caddy will get the SSL cert automatically on first request.

Headscale Configuration

mkdir -p config

Generate a base config:

docker run --rm headscale/headscale:latest headscale config > config/config.yaml

Or create it manually. Here’s a stripped-down version with the critical fields:

# config/config.yaml
server_url: https://headscale.yourdomain.com
listen_addr: 0.0.0.0:8080

# This is the "magic" that makes it work like Tailscale
dns:
  # Enable MagicDNS — your devices get .yourdomain.com names
  magic_dns: true
  base_domain: yourdomain.com

# Randomize client ports for NAT traversal
randomize_client_port: true

# ACLs: who can talk to whom
acls:
  - action: accept
    src: ["*"]
    dst: ["*:*"]

Critical fields:

  • server_url: Must be the public HTTPS URL. Clients connect here.
  • base_domain: Used for MagicDNS. If set to yourdomain.com, your devices get names like my-laptop.yourdomain.com.
  • acls: The default above allows all-to-all communication. You can restrict it later.

Start It

docker compose up -d

Check the logs:

docker logs headscale

You should see “Headscale is listening on :8080”. The Caddy container will also log “certificate obtained successfully” after a few seconds.

Creating Users and Enrolling Devices

Headscale uses a user/namespace system. Each user gets their own set of devices.

Create a User

docker exec headscale headscale users create homelab

I use homelab as my namespace. You could use your name, your organization, whatever.

Pre-Auth Key (The Easy Way)

Instead of the tailscale login flow (which redirects to tailscale.com by default), you use pre-authentication keys with Headscale.

docker exec headscale headscale preauthkeys create --user homelab --expiration 24h --reusable

This outputs a key like 1234abc.... Copy it.

Connect a Device

On any machine you want to join to your tailnet:

# Linux/macOS
sudo tailscale up --login-server=https://headscale.yourdomain.com --authkey=KEY_FROM_ABOVE

# Windows (PowerShell)
tailscale up --login-server=https://headscale.yourdomain.com --authkey=KEY_FROM_ABOVE

That’s it. Your device is now part of your private mesh network. You can verify:

tailscale status

You’ll see all your connected devices with their Tailscale IPs (typically 100.x.x.x range).

Pro tip: The first device you enroll becomes the “admin.” Make sure it’s a machine you control. And keep the auth keys temporary — I use 24h expiry and generate new ones when needed.

MagicDNS: Access Devices by Hostname

This is my favorite feature. With MagicDNS, you can ping my-laptop.yourdomain.com instead of remembering IPs.

Enable it in your Headscale config (we already did):

dns:
  magic_dns: true
  base_domain: yourdomain.com

On each client, you also need to accept the Tailscale DNS configuration:

sudo tailscale up --accept-dns

Now ping my-laptop.yourdomain.com works. So does SSH, HTTP, anything. It’s like having a local DNS that follows your devices wherever they go.

ACLs: Who Can Talk to Whom

By default, every device can talk to every other device. That’s Tailscale’s “flat network” model, and it’s great for most homelabs.

But you might want restrictions. Here’s an example ACL set:

acls:
  # Servers can talk to each other
  - action: accept
    src: ["tag:server"]
    dst: ["tag:server:*"]

  # Clients can talk to servers, but not to each other
  - action: accept
    src: ["tag:client"]
    dst: ["tag:server:*"]

  # Admin can access everything
  - action: accept
    src: ["tag:admin"]
    dst: ["*:*"]

  # Default deny
  - action: deny
    src: ["*"]
    dst: ["*:*"]

Tags are assigned to devices during enrollment:

sudo tailscale up --login-server=https://headscale.yourdomain.com --authkey=KEY --advertise-tags=tag:server

Note: Tag-based ACLs require the Headscale server to approve the tags. You do this in the admin UI or CLI.

I keep my setup simple: everything can talk to everything. My homelab isn’t that big, and the convenience outweighs the marginal security gain.

The Admin UI

Headscale has a web admin interface. It’s basic but functional.

Enable it by adding to your config.yaml:

# Enable experimental features
experimental:
  unix_socket: /var/run/headscale.sock
  # Enable the web interface
  web_interface: true

Then access https://headscale.yourdomain.com/web in your browser. Log in with your Headscale CLI credentials.

From the admin UI you can:

  • View all connected devices
  • Expire auth keys
  • Manage users
  • See connection status

It’s not as polished as Tailscale’s dashboard, but it covers the essentials.

Running Headscale Behind a Reverse Proxy

If you already run Traefik or Nginx Proxy Manager (both covered on this blog), you don’t need the Caddy container. Just point your reverse proxy at the Headscale container on port 8080.

Traefik labels:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.headscale.rule=Host(`headscale.yourdomain.com`)"
  - "traefik.http.routers.headscale.entrypoints=websecure"
  - "traefik.http.routers.headscale.tls.certresolver=letsencrypt"
  - "traefik.http.services.headscale.loadbalancer.server.port=8080"

Make sure to set server_url: https://headscale.yourdomain.com in the config.

What I Learned the Hard Way

1. DNS propagation takes patience

When I first set up Headscale, I changed my DNS records and expected everything to work instantly. It didn’t. Some ISPs cache DNS for hours. My phone, which uses 1.1.1.1, resolved immediately. My home router took 45 minutes.

Set up the DNS records before deploying the containers. Give it time.

2. Auth keys expire — plan for it

I generated an auth key with no expiry for convenience. Then I forgot about it. Months later, I realized anyone who found that key could join my tailnet. Rotate your keys. Use 24h or 7d expiry. Generate new ones when needed.

3. Mobile clients need the official app

The Android and iOS Tailscale apps work with Headscale, but the setup is manual. You can’t scan a QR code like with the standard Tailscale flow. You need to:

  1. Open the Tailscale app
  2. Go to Settings → Account → Login with custom server
  3. Enter your Headscale URL: https://headscale.yourdomain.com
  4. Paste the auth key from the CLI

It’s a few extra taps, but it works perfectly after that.

4. Don’t run multiple Headscale instances for HA (yet)

I tried setting up two Headscale servers with a shared database for high availability. Big mistake. The database schema isn’t designed for concurrent writes from multiple instances. Stick to one server with good backups.

5. Backups are trivial but critical

Here’s my backup command:

tar -czf headscale-backup-$(date +%Y%m%d).tar.gz ~/headscale/config ~/headscale/data

That’s it. Restore by:

  1. Stop Headscale
  2. Extract the backup
  3. Restart

Your entire tailnet configuration — users, devices, keys — is preserved.

Headscale vs Netbird vs Traditional WireGuard

I get asked this a lot, so here’s my quick take:

FeatureHeadscaleNetbirdVanilla WireGuard
Setup complexityMediumMediumHigh
NAT traversalAutomaticAutomaticManual (requires public IP + port forwarding)
Mesh networkingYesYesManual (full mesh = N configs)
ACLsYAML-basedDashboardManual iptables
Mobile appsOfficial Tailscale appOfficial appThird-party (WireGuard app)
Control planeSelf-hostedCloud or self-hostedNone (peer-to-peer config)
PerformanceWireGuard-nativeWireGuard-nativeWireGuard-native

Pick Headscale if: You already use Tailscale and want the same experience with your own server.

Pick Netbird if: You prefer a UI-driven setup with built-in SSH and firewall management.

Pick vanilla WireGuard if: You need maximum performance, have static public IPs, and don’t mind managing config files.

I use all three for different purposes. Headscale for my personal mesh. Netbird for a client project that needs a management dashboard. Vanilla WireGuard for a site-to-site link between two fixed servers.

Is This Worth It?

Honestly? For most people, Tailscale’s free tier is enough. You don’t need Headscale. Their cloud is reliable, their product is excellent, and the free plan is generous.

But if you’re the kind of person who reads this blog — someone who wants control, who runs their own servers, who doesn’t trust “the cloud” with their infrastructure metadata — then Headscale is for you.

The peace of mind is real. Knowing that no third-party cloud service is involved in my mesh networking lets me sleep better. And the geek factor of running my own coordination server? That’s a bonus.

Your Next Steps

  1. Set up Headscale on a VPS or home server using the Docker Compose above
  2. Enroll your first device and verify connectivity
  3. Enable MagicDNS and stop typing IP addresses forever
  4. Set up backups — one command, once a day
  5. Tell me how it goes. I’m curious what setups you’re running with this.

Related Articles:

Written on July 6, 2026. Running Headscale on a Hetzner CX22, managing 12 devices across 4 locations. Zero issues in 6 months.

Stay in the loop 📬

Get self-hosting tutorials, tool reviews, and infrastructure tips delivered to your inbox. No spam, unsubscribe anytime.

Join 0 self-hosters. Free forever.