Tailscale makes connecting devices dead simple — install the client, authenticate, and every machine on your tailnet can reach every other machine over an encrypted WireGuard tunnel. No port forwarding, no dynamic DNS, no public IP required.
The catch: the official Tailscale control server is proprietary and hosted in Tailscale’s cloud. While the free tier is generous, you’re trusting someone else’s infrastructure with your network topology metadata, and the free tier caps at 100 devices with 3 users.
Enter Headscale — an open-source, self-hosted implementation of the Tailscale control server. You run the coordination plane on your own hardware, use the same official Tailscale clients on every device, and keep full control over authentication, ACLs, and routing metadata.
This guide covers deploying Headscale with Docker Compose, securing it behind Traefik with Let’s Encrypt, registering clients, defining ACLs for network segmentation, and enabling subnet routing so your entire homelab LAN becomes reachable from anywhere.
How Headscale and Tailscale Work
Understanding the architecture helps when things don’t work.
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Laptop │ │ Headscale │ │ Phone │
│ tailscale │◄────►│ Control Plane │◄────►│ tailscale │
│ 10.0.0.2 │ │ 10.0.0.1 │ │ 10.0.0.3 │
└──────┬───────┘ └──────────────────┘ └──────┬───────┘
│ │
│ WireGuard Direct │
└────────────────────────────────────────────────┘
(NAT traversal)
- Control Plane (Headscale): Authenticates devices, exchanges public WireGuard keys, distributes the network map. Your traffic never passes through it — it’s just the phone book.
- Data Plane (Tailscale client): Each device establishes direct WireGuard tunnels to every other device. Traffic is peer-to-peer, end-to-end encrypted, with NAT traversal via STUN and DERP relay servers as fallback.
- Private IP allocation: Every node gets a unique IP in the
Tailscale range (default
100.64.0.0/10). These IPs are reachable from any connected device.
Headscale replaces only the control plane. The clients are the same official Tailscale clients — no proprietary fork needed.
Step 1 — Prerequisites
You need:
- A server with Docker and Docker Compose (a $5 VPS or your Proxmox host works fine)
- A domain name pointing to the server’s public IP
- Port 443 (HTTPS) reachable from the internet
- Traefik or another reverse proxy already running (this guide assumes Traefik, but any reverse proxy works)
Headscale is extremely lightweight. A single-core VM with 512MB RAM handles 50+ devices without breaking a sweat — the control plane exchanges key material and coordinates routes but never touches traffic.
Step 2 — Directory Structure and Configuration
|
|
Download the Headscale configuration file for the latest stable release:
|
|
Edit config/config.yaml with the minimal changes needed:
|
|
Key configuration details:
server_url: Must be the HTTPS URL clients use to reach Headscale. Change this to your actual domain.magic_dns: When enabled, every device gets a DNS name likelaptop.example-headscale.ts.net. Clients can resolve each other by hostname instead of IP.nameservers: Push your homelab DNS servers to connected clients so internal hostnames resolve on the VPN.
Step 3 — Docker Compose Deployment
|
|
About this setup:
ports: 127.0.0.1:8080:8080: Only binds to localhost. Headscale is NOT exposed directly — your reverse proxy handles external traffic. This is a critical security boundary.read_only: true: The container filesystem is immutable. Runtime data lives in thetmpfsmount (for PID/socket files) and thelibvolume (for the database and keys).- Port 9090: gRPC endpoint for CLI operations. Only needed for
docker execcommands.
Start it:
|
|
Verify it’s healthy:
|
|
Step 4 — Traefik Reverse Proxy Configuration
If you’re already running Traefik (see the Gitea or Authentik deployment guides on this blog), add a new router and service:
|
|
Or in Docker Compose with labels (if Traefik auto-discovers):
|
|
If you use Nginx Proxy Manager or Caddy, the setup is similar:
proxy pass to http://127.0.0.1:8080, enable WebSocket support,
and terminate TLS with Let’s Encrypt.
Verify the public endpoint:
|
|
Step 5 — Create Users and Namespaces
Headscale groups devices by users (previously called namespaces in older versions). Devices under the same user can see each other by default.
|
|
Output:
ID | Name | Created
1 | homelab | 2026-05-19 17:05:00
2 | servers | 2026-05-19 17:05:00
3 | iot | 2026-05-19 17:05:00
Why separate users? Devices in different users are isolated. Use this to segment trusted devices (your laptop, phone) from untrusted ones (IoT widgets, guest VMs).
Step 6 — Register Client Devices
Method A: Interactive Registration (for laptops and workstations)
Install the Tailscale client on your device:
|
|
On the client, start the connection pointing at your Headscale server:
|
|
The client prints an authentication URL:
To authenticate, visit:
https://headscale.example.org/register/STqfQR4wnKr-eerDXam3_RYi
Open that URL in a browser. It shows the node key and a command to run on your headscale server:
|
|
Verify the node is connected:
|
|
Output:
ID | Hostname | User | IP addresses | Connected | Last seen
1 | laptop | homelab | 100.64.0.1, fd7a:... | Yes | 2026-05-19 17:10
From the laptop, test connectivity:
|
|
Method B: Pre-Auth Keys (for servers and automation)
Pre-auth keys let you register devices without interactive authentication — perfect for CI/CD, deployment scripts, or headless servers:
|
|
On the server node:
|
|
The node registers silently and appears in the list immediately.
Key options:
| Flag | Purpose |
|---|---|
--reusable |
Use the same key for multiple devices |
--ephemeral |
Node disappears from the list when it disconnects |
--expiration 24h |
Key auto-expires (recommended for security) |
Step 7 — Subnet Routing (Access Your Full LAN)
By default, Tailscale only gives you access to the Tailscale IPs of connected devices. If you want to reach your entire 10.0.20.0/24 homelab LAN from a remote laptop, enable subnet routing.
7a — Configure the Exit Node
On the server that sits on your homelab LAN (e.g., your Proxmox host), advertise the subnet:
|
|
7b — Approve Routes on Headscale
|
|
7c — Accept Routes on Clients
On each client that needs LAN access:
|
|
Now from your laptop anywhere in the world:
|
|
Your entire LAN is reachable as if you were sitting at home.
Exit Node (Full Tunnel)
For full traffic routing (like a traditional VPN), advertise as an exit node:
|
|
Approve the 0.0.0.0/0 route on Headscale:
|
|
Clients can then use this node as an exit node:
|
|
All traffic routes through your home connection — useful for public Wi-Fi security or accessing geo-restricted services.
Step 8 — Access Control Lists (ACLs)
Without ACLs, any device can talk to any other device. ACLs let you define who can talk to whom with a simple declarative syntax.
Create /opt/headscale/config/acls.hujson:
|
|
Reference this file in config.yaml:
|
|
Then apply and test:
|
|
ACL best practices:
- Start restrictive: Default deny, then grant specific access
- Use groups: Group devices by function, not by hostname
- Protocol-aware: Tag-based ACLs let you match on Tailscale tags instead of user membership
- Test changes: Run
acl debugbefore deploying to prevent accidental lockouts
Step 9 — DERP Relay Servers
Tailscale prefers direct peer-to-peer connections, but when NAT traversal fails (symmetric NAT, strict firewalls), it falls back to DERP (Detour Encrypted Routing Protocol) relay servers.
Headscale includes a built-in DERP server. Enable it in
config.yaml:
|
|
Add the STUN port to your firewall:
|
|
If you don’t want to depend on Tailscale’s DERP servers at all
(self-contained tailnet with no external dependencies), set
urls: [] — but this breaks connectivity between clients behind
symmetric NAT that can’t establish direct WireGuard tunnels.
Step 10 — Production Hardening
10a — Regular Backups
Your Headscale data is in /opt/headscale/lib/. Back up the entire
directory:
|
|
If you lose this data, all registered devices need to re-authenticate. The database contains the WireGuard public keys, node assignments, and ACL configuration.
10b — Monitoring and Alerts
Add health checks to your monitoring stack:
|
|
Grafana dashboard metrics to watch:
headscale_nodes_total— registered device countheadscale_nodes_online— currently connected devicesheadscale_requests_total— API request volume- Certificate expiry (from Traefik)
10c — Key Rotation
|
|
10d — Restrict gRPC Access
The gRPC port (9090) is bound to localhost by default and should stay that way. If you need remote CLI access, tunnel through SSH:
|
|
Step 11 — Client Tuning Tips
macOS/iOS clients
Add DNS search domains for your homelab:
|
|
Linux server clients (no GUI)
|
|
Docker containers on the tailnet
Use a Tailscale sidecar container for Docker services:
|
|
Then route traffic through the sidecar’s Tailscale IP. This gives each Docker service its own stable mesh IP without installing anything on the host.
Why Headscale Over Direct WireGuard or Tailscale SaaS
| Feature | Headscale | Tailscale SaaS | WireGuard only |
|---|---|---|---|
| Control plane | Self-hosted | Cloud-only | None (manual) |
| Client count | Unlimited | 100 (free) | Unlimited |
| NAT traversal | Built-in | Built-in | Manual (STUN) |
| ACLs | Declarative HuJSON | Web UI + ACLs | iptables/nftables |
| MagicDNS | Yes | Yes | No |
| Subnet routing | Yes | Yes | Manual routes |
| Exit nodes | Yes | Yes | Manual NAT |
| Data sovereignty | Full | None | Full |
| Setup complexity | Medium | Low | High |
| Cost | VPS + domain | Free tier | Free |
Choose Headscale when: You want Tailscale’s zero-config WireGuard mesh without depending on Tailscale’s infrastructure, need unlimited devices, or want full ACL control with declarative policies.
Choose Tailscale SaaS when: You want the easiest possible setup, don’t mind the dependency, and your device count is under 100.
Choose raw WireGuard when: You need a simple site-to-site VPN with a known peer count and can manage keys manually.
Troubleshooting
Clients can’t connect
|
|
Nodes show “offline” in the list
|
|
MagicDNS not resolving
|
|
ACL change not taking effect
|
|
Summary
Headscale gives you the best of both worlds: Tailscale’s zero-config WireGuard mesh with WireGuard’s full data sovereignty. Running it in Docker Compose with Traefik is straightforward and maintainable:
- Deploy Headscale behind a reverse proxy on port 443
- Create users to segment devices logically
- Register clients interactively or with pre-auth keys
- Enable subnet routing to reach your full homelab LAN
- Define ACLs with HuJSON for least-privilege access
- Add DERP relay for reliable NAT traversal
- Back up the lib directory — without it, every device re-authenticates
The result is a mesh VPN that connects your laptop, phone, servers, and IoT devices into a single private network with zero manual WireGuard key management, no port forwarding, and no third-party dependency.