Tailscale has changed how people think about VPNs. Instead of wrestling with traditional hub-and-spoke configurations, you get a mesh network where every device can talk directly to every other device, authenticated by identity rather than IP addresses. But Tailscale's coordination server is proprietary, your device keys and access policies live on someone else's infrastructure, and the free tier has limits that grow inconvenient fast. Headscale is the open-source, self-hosted alternative that replaces Tailscale's coordination server while keeping full compatibility with every official Tailscale client. You run the control plane on your own Ubuntu VPS, and you own everything: your keys, your ACLs, your DNS, your network topology.
This guide walks you through deploying Headscale on an Ubuntu VPS from scratch. You will configure a reverse proxy with TLS, register users and devices, set up access control lists, enable MagicDNS for human-readable device names, configure exit nodes and subnet routing, and integrate Headscale with self-hosted services like Immich, Gitea, and Grafana over a private mesh network. By the end, you will have a production-ready private VPN that costs a fraction of commercial alternatives and gives you complete control over your network.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $19.80/mo
Want fully managed hosting? — we handle everything
Tailscale vs WireGuard vs Headscale: Understanding the Mesh VPN Model
Traditional VPNs like OpenVPN and IPSec use a hub-and-spoke model. All traffic flows through a central server, which becomes both a bottleneck and a single point of failure. WireGuard improved on this dramatically with its lean kernel-level protocol, fast handshakes, and modern cryptography, but configuring a mesh network with WireGuard still requires manually exchanging public keys between every pair of devices and maintaining static endpoint configurations. Add ten devices and you are managing 45 peer relationships by hand.
Tailscale built a layer on top of WireGuard that automates all of this. A coordination server handles key exchange, NAT traversal (via DERP relay servers), and access policy enforcement. Each device runs the Tailscale client, which negotiates direct WireGuard tunnels to every other device. The coordination server never sees your traffic; it only facilitates the initial handshake. The result is a fully meshed, peer-to-peer encrypted network that works seamlessly across NATs, firewalls, and different networks.
Headscale is a drop-in replacement for Tailscale's proprietary coordination server. Written in Go, it implements the same coordination protocol so every official Tailscale client (Linux, macOS, Windows, iOS, Android) works without modification. You get the same mesh VPN experience, the same NAT traversal, the same MagicDNS, but the control plane runs on your infrastructure under your control. No device limits, no vendor lock-in, no third-party access to your network topology.
- WireGuard alone gives you fast, secure point-to-point tunnels but requires manual configuration for every peer relationship
- Tailscale automates WireGuard mesh networking through a proprietary coordination server with usage limits on the free plan
- Headscale provides the same automation and client compatibility as Tailscale, but you own and operate the coordination server yourself
When to Choose Headscale Over Plain WireGuard
If you have two or three servers that need a static tunnel between them, plain WireGuard is simple and effective. Our WireGuard VPN setup guide covers that use case in detail. But once your needs grow beyond basic site-to-site connections, Headscale starts to make significantly more sense.
Choose Headscale when you need to:
- Connect many devices dynamically. Adding a new phone, laptop, or server to a WireGuard mesh means updating every existing peer's config. With Headscale, you run one command on the new device and it joins the network automatically.
- Handle NAT traversal. WireGuard requires at least one endpoint to have a public IP or manually configured port forwarding. Headscale and the Tailscale client handle NAT hole-punching and DERP relay fallback automatically.
- Enforce access policies. WireGuard has no concept of ACLs. Every peer can reach every other peer. Headscale lets you define granular rules: which users can access which devices on which ports.
- Support mobile devices. The official Tailscale apps for iOS and Android work directly with Headscale, giving you a polished mobile VPN experience without building anything custom.
- Use MagicDNS. Instead of remembering WireGuard tunnel IPs like
100.64.0.5, you can reach devices by name:workstation.example.com.
The tradeoff is that Headscale adds a dependency: the coordination server must be reachable for new devices to join or for existing devices to re-authenticate. Existing tunnels continue working even if the coordination server goes offline temporarily, but you want it running reliably. This is where a VPS with high-availability infrastructure matters.
Prerequisites
Before starting, make sure you have the following in place:
- An Ubuntu VPS running Ubuntu 22.04 LTS or 24.04 LTS. Headscale is remarkably lightweight: it uses under 100MB of RAM and minimal CPU. A MassiveGRID VPS with 1 vCPU and 1GB RAM comfortably handles hundreds of connected devices.
- A domain name with DNS access. You will need to point a subdomain (e.g.,
hs.example.com) to your VPS IP address via an A record. - Root or sudo access to the VPS.
- A basic firewall configured with ports 80 and 443 open for the reverse proxy, and optionally port 3478/UDP for STUN.
SSH into your VPS and update the system before proceeding:
sudo apt update && sudo apt upgrade -y
Installing Headscale on Ubuntu
You can install Headscale either as a native binary or via Docker. Both approaches work well, but the native installation is simpler for a dedicated coordination server.
Option A: Native Installation (Recommended)
Download the latest Headscale release from GitHub. Check the releases page for the most recent version and adjust the version number accordingly:
HEADSCALE_VERSION="0.23.0"
wget -O headscale.deb "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64.deb"
sudo dpkg -i headscale.deb
This installs the Headscale binary and creates a systemd service file. Before starting the service, you need to configure it. Create the configuration directory and edit the config file:
sudo mkdir -p /etc/headscale
sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.bak
sudo nano /etc/headscale/config.yaml
The key settings to configure:
server_url: https://hs.example.com
listen_addr: 127.0.0.1:8080
metrics_listen_addr: 127.0.0.1:9090
private_key_path: /var/lib/headscale/private.key
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
derp:
server:
enabled: false
urls:
- https://controlplane.tailscale.com/derpmap/default
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
dns:
magic_dns: true
base_domain: example.com
nameservers:
global:
- 1.1.1.1
- 9.9.9.9
Note that listen_addr is set to 127.0.0.1:8080 because you will place Nginx in front of it. The server_url must match the domain you will configure with SSL. Now enable and start the service:
sudo systemctl enable headscale
sudo systemctl start headscale
sudo systemctl status headscale
Option B: Docker Installation
If you prefer containerized deployments, create a directory structure and a docker-compose.yml:
mkdir -p ~/headscale/config ~/headscale/data
cat << 'EOF' > ~/headscale/docker-compose.yml
services:
headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
ports:
- "127.0.0.1:8080:8080"
- "127.0.0.1:9090:9090"
command: serve
EOF
Copy the default configuration into ~/headscale/config/config.yaml, adjust the same settings described above, then start the container:
cd ~/headscale
docker compose up -d
docker compose logs -f headscale
Nginx Reverse Proxy with SSL
Headscale requires HTTPS for client connections. You will use Nginx as a reverse proxy with Let's Encrypt certificates. If you are not yet familiar with this pattern, our Nginx reverse proxy guide covers the fundamentals in depth.
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginx
Create an Nginx configuration for your Headscale domain:
sudo nano /etc/nginx/sites-available/headscale
Add the following configuration:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name hs.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
The WebSocket upgrade headers are critical because the Tailscale control protocol uses long-lived connections. The extended timeouts prevent Nginx from closing these connections prematurely. Enable the site and obtain an SSL certificate:
sudo ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d hs.example.com
Certbot will modify your Nginx config to add the SSL directives and set up automatic certificate renewal. Verify the result:
curl -s https://hs.example.com/health | head
You should see a response indicating the server is healthy.
Creating Users and Pre-Auth Keys
Headscale organizes devices under users (previously called "namespaces"). Create your first user:
sudo headscale users create alice
To register devices without interactive login, generate pre-authentication keys. These are tokens you provide to the Tailscale client to register a device directly:
# Create a single-use key that expires in 24 hours
sudo headscale preauthkeys create --user alice --expiration 24h
# Create a reusable key (useful for automation or onboarding multiple devices)
sudo headscale preauthkeys create --user alice --reusable --expiration 720h
List existing keys to verify:
sudo headscale preauthkeys list --user alice
You can create multiple users for different people or purposes. For example, separate users for family members, a dedicated user for server infrastructure, or one for IoT devices:
sudo headscale users create servers
sudo headscale users create family
sudo headscale users create iot
This user separation becomes valuable when you configure ACLs later, as you can write rules that apply to all devices belonging to a specific user.
Connecting Devices to Your Headscale Network
The beauty of Headscale is that you use the official Tailscale clients on every platform. The only difference is pointing them at your Headscale server instead of Tailscale's.
Linux
Install the Tailscale client using the official script:
curl -fsSL https://tailscale.com/install.sh | sh
Connect to your Headscale instance using a pre-auth key:
sudo tailscale up --login-server https://hs.example.com --authkey YOUR_PREAUTH_KEY
Or connect interactively (you will get a URL to register the device manually):
sudo tailscale up --login-server https://hs.example.com
When using the interactive method, copy the provided node key and register it on the server:
sudo headscale nodes register --user alice --key nodekey:abc123...
macOS
Install the Tailscale app from the Mac App Store or via Homebrew (brew install tailscale). To point it at your Headscale server, you need to set a custom control URL. Open Terminal and run:
tailscale login --login-server https://hs.example.com
For the GUI app, hold the Option key while clicking the Tailscale icon in the menu bar, then select "Use Custom Coordination Server" and enter your Headscale URL.
Windows
Download and install the official Tailscale client from tailscale.com. Before signing in, open a Command Prompt or PowerShell as Administrator and set the login server:
tailscale login --login-server https://hs.example.com
Alternatively, you can set a registry key before first launch to configure the control URL permanently:
reg add "HKLM\SOFTWARE\Tailscale IPN" /v LoginURL /t REG_SZ /d "https://hs.example.com" /f
iOS and Android
Install the official Tailscale app from the App Store or Google Play. On both platforms, you can configure a custom control server URL through the app's settings or by navigating to your Headscale URL in a mobile browser, which triggers the app to open with the correct server pre-configured. On iOS, you can also use an MDM configuration profile to set the control URL.
After connecting devices, verify they appear in your Headscale node list:
sudo headscale nodes list
You should see each device with its assigned Tailscale IP, hostname, user, and last seen timestamp.
Access Control Lists: Defining Who Can Reach What
By default, Headscale allows all devices to communicate with each other. In a personal setup this might be fine, but for team or multi-user environments you will want granular access policies. Headscale uses ACL policies in the same JSON format as Tailscale.
Create or edit the ACL policy file:
sudo nano /etc/headscale/acl.json
Here is an example policy that separates server infrastructure from personal devices:
{
"groups": {
"group:admins": ["alice"],
"group:servers": ["servers"],
"group:family": ["family"]
},
"acls": [
{
"action": "accept",
"src": ["group:admins"],
"dst": ["*:*"]
},
{
"action": "accept",
"src": ["group:servers"],
"dst": ["group:servers:*"]
},
{
"action": "accept",
"src": ["group:family"],
"dst": [
"group:servers:80",
"group:servers:443",
"group:servers:53"
]
}
]
}
This policy does three things:
- Admins (alice) can access everything on every port.
- Servers can communicate freely with other servers (for inter-service communication).
- Family members can only reach servers on ports 80, 443, and 53 (web services and DNS).
Reference the ACL file in your Headscale config:
acl_policy_path: /etc/headscale/acl.json
Restart Headscale to apply:
sudo systemctl restart headscale
ACL policies are evaluated in order, and the first matching rule wins. Test your policies by attempting connections between devices in different groups to verify the rules work as expected.
MagicDNS: Human-Readable Device Names
With MagicDNS enabled in your Headscale configuration, every device on your network becomes reachable by name instead of IP address. If you set base_domain: example.com in the config, a device registered as "workstation" under user "alice" becomes reachable at workstation.alice.example.com.
MagicDNS works by having the Tailscale client configure the device's DNS resolver to query Headscale for names within the base domain. This means you can use these names in browser URLs, SSH commands, configuration files, and service definitions without hardcoding IP addresses:
# SSH by name instead of IP
ssh workstation.alice.example.com
# Access services by name
curl http://nas.servers.example.com:8080
# Use in Docker Compose or config files
DATABASE_HOST=postgres.servers.example.com
You can also configure Headscale to override DNS for specific domains, which is useful for split-DNS setups where internal domains resolve to private IPs on your tailnet but public IPs outside it. Add custom DNS entries in the Headscale configuration:
dns:
magic_dns: true
base_domain: example.com
nameservers:
global:
- 1.1.1.1
restricted:
internal.company.com:
- 100.64.0.10
This tells the Tailscale client to resolve *.internal.company.com using the DNS server at 100.64.0.10 (a device on your tailnet running a local DNS resolver) while using Cloudflare for everything else.
Exit Nodes: Routing Internet Traffic Through Your VPN
An exit node routes all of a device's internet traffic through another device on the tailnet. This is useful when you want to appear to browse from your VPS's IP address, access geo-restricted content, or secure your traffic on untrusted networks like public Wi-Fi.
On the device you want to use as an exit node (typically your VPS), enable IP forwarding and advertise it as an exit node:
# Enable IP forwarding
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
# Advertise as exit node
sudo tailscale up --login-server https://hs.example.com --advertise-exit-node
On the Headscale server, approve the exit node route:
sudo headscale routes list
sudo headscale routes enable -r <ROUTE_ID>
On client devices that want to use the exit node, specify it when connecting:
# Linux
sudo tailscale up --login-server https://hs.example.com --exit-node=vps.servers.example.com
# Or use the Tailscale GUI on macOS/Windows/mobile to select the exit node
Your internet traffic now flows through the VPS's encrypted tunnel. Verify by checking your public IP from the client:
curl ifconfig.me
It should show your VPS's IP address instead of your local one.
Subnet Routing: Accessing LAN Devices Through the VPN
Subnet routing lets you access devices on a local network through a Tailscale node on that network, even if those local devices do not have Tailscale installed. This is perfect for reaching printers, NAS devices, IoT gadgets, or any hardware that cannot run the Tailscale client.
On a device that is connected to both your tailnet and the local network you want to expose, advertise the subnet:
# Enable IP forwarding (if not already done)
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
# Advertise the local subnet
sudo tailscale up --login-server https://hs.example.com --advertise-routes=192.168.1.0/24
Approve the route on the Headscale server:
sudo headscale routes list
sudo headscale routes enable -r <ROUTE_ID>
Now any device on your tailnet can reach 192.168.1.x addresses through the subnet router. Access your home NAS at 192.168.1.50 from anywhere in the world, securely, without exposing any ports to the internet.
You can advertise multiple subnets from a single device:
sudo tailscale up --login-server https://hs.example.com --advertise-routes=192.168.1.0/24,10.0.0.0/24
Combine subnet routing with ACLs to control which users can access which subnets. For example, allow admins to reach the server VLAN at 10.0.0.0/24 but restrict family members to only the home network at 192.168.1.0/24.
Integrating with Self-Hosted Services
One of the most compelling uses of Headscale is connecting self-hosted services over a private mesh network. Instead of exposing services to the public internet with reverse proxies and firewall rules, you access them exclusively through your tailnet. No open ports, no public DNS records, no attack surface.
Immich (Photo Management)
If you run Immich on a VPS or home server, bind it to the Tailscale interface or 0.0.0.0 and access it via the device's tailnet address. Your phone's Tailscale client gives it a direct, encrypted connection to Immich for photo backup without any public exposure:
# In your Immich docker-compose.yml, the default port binding works:
# ports:
# - "2283:3001"
# Access via: http://photoserver.servers.example.com:2283
Gitea (Git Hosting)
Run your own Git server accessible only to devices on your tailnet. Clone, push, and pull over SSH or HTTPS using the MagicDNS name:
git clone http://gitea.servers.example.com:3000/alice/my-project.git
git remote set-url origin http://gitea.servers.example.com:3000/alice/my-project.git
Grafana (Monitoring)
Monitor your infrastructure through Grafana dashboards that are only accessible over your private network. Point Grafana's data sources at other services on the tailnet using their MagicDNS names:
# grafana datasource config
datasources:
- name: Prometheus
type: prometheus
url: http://monitoring.servers.example.com:9090
The pattern extends to any self-hosted service: Nextcloud, Vaultwarden, Home Assistant, Jellyfin, Paperless-ngx. You eliminate the entire class of security concerns around public-facing services. No TLS certificates to manage for internal services, no rate limiting, no brute-force protection, because the services simply are not reachable from the internet.
For services that need to coexist with public traffic, you can use the Tailscale IP as an additional listener. Nginx can be configured to serve public traffic on the external IP while also being reachable on the Tailscale IP for administrative access.
Headscale UI: Managing Your Network Visually
While the headscale CLI is powerful, a visual interface makes day-to-day management easier. Several community-built web UIs are available for Headscale. The most popular is headscale-ui, a lightweight single-page application that connects to the Headscale gRPC API.
Deploy headscale-ui alongside your Headscale instance:
docker run -d \
--name headscale-ui \
--restart unless-stopped \
-p 127.0.0.1:8443:443 \
ghcr.io/gurucomputing/headscale-ui:latest
Add a separate Nginx server block or location block to proxy the UI, and restrict access to your tailnet IP range for security:
server {
listen 80;
server_name headscale-ui.example.com;
allow 100.64.0.0/10;
deny all;
location / {
proxy_pass https://127.0.0.1:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
By restricting access to the 100.64.0.0/10 range (Tailscale IPs), the UI is only accessible from devices on your tailnet. Generate an API key for the UI to authenticate with Headscale:
sudo headscale apikeys create --expiration 90d
Enter this API key in the headscale-ui settings page. The UI lets you manage users, view connected nodes, approve routes, inspect ACL policies, and generate pre-auth keys, all from a browser window.
Your Coordination Server Needs 100% Uptime
Headscale itself is lightweight. The binary uses under 100MB of RAM and negligible CPU, even with hundreds of connected devices. But the coordination server is a critical piece of your network infrastructure. If it goes down, existing WireGuard tunnels continue working (they are peer-to-peer), but new devices cannot join, existing devices cannot re-authenticate when their keys expire, and route or ACL changes cannot propagate.
This makes the underlying infrastructure important. You want your Headscale VPS to have:
- Automatic failover. If the physical node hosting your VPS fails, it should automatically restart on another node without manual intervention.
- Replicated storage. Your Headscale database (SQLite file with all device registrations, keys, and policies) needs to survive disk failures.
- DDoS protection. Your coordination server's domain and IP should be protected from volumetric attacks that could make it unreachable.
- Reliable networking. Low-latency, high-uptime connectivity ensures fast key exchanges and route updates across your mesh.
A MassiveGRID VPS with 1 vCPU and 1GB RAM is more than sufficient for Headscale alone. The Proxmox HA cluster provides automatic failover, Ceph's 3x NVMe replication protects your data, and the 100% uptime SLA means your coordination server stays reachable. If you are running Headscale alongside other services like a reverse proxy, monitoring stack, or lightweight web applications, consider a Dedicated VPS starting at $19.80/mo for guaranteed CPU and RAM resources that are not shared with other tenants.
For business-critical VPN deployments where the coordination server underpins team access to production infrastructure, MassiveGRID's fully managed hosting takes operational burden off your plate entirely. The team handles OS updates, security patching, monitoring, and incident response so your coordination server stays healthy around the clock.
Prefer a Managed VPN? When Headscale Is Not the Right Fit
Self-hosting Headscale gives you maximum control and eliminates per-device costs, but it is not the right choice for every situation. You are responsible for keeping the server updated, monitoring its health, managing SSL certificate renewals, and handling backups of the database. If the coordination server has an issue at 3 AM, you are the one who needs to fix it.
For small teams or individuals who want the mesh VPN experience without operational overhead, Tailscale's managed service is a perfectly reasonable choice. The free tier supports up to 100 devices with 3 users. For larger teams, Tailscale's paid plans handle everything: coordination, DERP relays, admin console, SSO integration, and audit logging.
The decision often comes down to these factors:
- Choose Headscale if you want full control over your network metadata, need more devices or users than free tiers allow, have compliance requirements that prohibit third-party coordination servers, or simply prefer self-hosting on principle.
- Choose managed Tailscale if you want zero operational overhead, need enterprise features like SSO and audit logs out of the box, or have a small team that fits within the free tier limits.
- Choose plain WireGuard if you only need static tunnels between a few servers and do not need the mesh networking, NAT traversal, or mobile client features that Tailscale and Headscale provide.
If you do choose to self-host, the investment is modest. A single low-cost VPS, an hour of initial setup, and occasional maintenance in exchange for a private, unlimited mesh VPN that you control completely. Headscale's active development community continues to close the feature gap with Tailscale, and with official client compatibility across every major platform, the experience for end users is virtually indistinguishable from the commercial offering.
Whether you deploy Headscale as a standalone coordination server on a lightweight VPS, bundle it with other self-hosted services on a dedicated resource VPS, or hand the operational responsibility to MassiveGRID's managed team, you get a mesh VPN that scales with your needs and keeps your network private by design.