Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Self-Host Tailscale with Headscale

By Kokil Thapa | Last reviewed: September 2026

You need secure remote access to staging servers, office LANs, and client infrastructure without exposing SSH to the public internet. Tailscale solves that with a zero-config mesh VPN, but the hosted control plane adds per-user cost and stores your network map on someone else's servers. When you Self-Host Tailscale with Headscale, you keep the same Tailscale client while running the coordination server yourself—ideal for agencies, small DevOps teams, and anyone who already manages Linux servers in Nepal or abroad. This guide walks through a production-ready Headscale install on Ubuntu 24.04 with Docker, ACLs, DNS, and the operational habits that keep the mesh stable.

What Is Headscale and Why Self-Host Tailscale with Headscale?

Headscale is an open-source, self-hosted implementation of the Tailscale coordination server. It does not replace the Tailscale client on your laptops and servers. Instead, it replaces Tailscale's cloud control plane—the component that distributes node keys, ACL policies, and DNS settings across your mesh.

The official Tailscale client still handles WireGuard tunnels, NAT traversal, and peer discovery. Headscale only coordinates who is allowed to talk to whom. That split matters when you read the docs: client behaviour comes from Tailscale's installation guides; server behaviour comes from the Headscale project on GitHub.

Headscale Control Plane ArchitectureHeadscale ServerACLs, DNS, node registryLaptop NodeTailscale clientEC2 ServerTailscale clientOffice LANSubnet routerWireGuard Mesh (peer-to-peer tunnels)Direct encrypted traffic between nodes
Self-Host Tailscale with Headscale: Headscale coordinates nodes; WireGuard carries traffic directly between peers.

On real client projects I maintain several sister sites on shared EC2 infrastructure with Deployer 7 and GitLab CI. SSH bastions work, but a mesh VPN simplifies ad-hoc access to staging boxes and internal services. For a deeper comparison of mesh VPN versus jump hosts, see our guide on SSH bastion host patterns and the companion piece on Tailscale zero-config mesh VPN.

When Headscale Makes Sense

  • You have five to five hundred nodes and want predictable cost (your VPS bill, not per-seat SaaS pricing).
  • Compliance or client policy requires the network map and ACL definitions to stay on infrastructure you control.
  • You already run Ubuntu servers and want one more lightweight service alongside existing stacks.
  • You need custom OIDC integration with your identity provider without Tailscale's enterprise tier.

When to Stay on Tailscale SaaS

Hosted Tailscale includes MagicDNS, subnet routing UI, and support out of the box. If your team is two people and billing is not a concern, SaaS is simpler. Headscale adds operational responsibility: backups, TLS renewal, upgrades, and debugging coordination failures yourself.

CriteriaTailscale SaaSHeadscale Self-Hosted
Control plane locationTailscale cloudYour VPS or on-prem server
Cost modelPer-user/month (free tier capped)Server cost only (~Rs 800–2,500/mo, ~USD 6–18)
ACL managementWeb admin + APIHuJSON policy file + CLI
Operational burdenLowMedium—backups, TLS, upgrades
Client compatibilityOfficial Tailscale clientSame official client
Subnet routers / exit nodesFull supportSupported with manual config

What Server Specs and Prerequisites Do You Need for Headscale?

Headscale is lightweight. A 1 vCPU, 1 GB RAM Ubuntu 24.04 VPS handles dozens of nodes comfortably. SQLite is fine for small deployments; switch to PostgreSQL when you expect hundreds of nodes or want easier backup tooling.

Before you install anything, prepare these items:

  1. A public hostname, e.g. headscale.example.com, with an A record pointing at your server.
  2. TLS certificate—Let's Encrypt via Caddy or Certbot on Nginx.
  3. UDP port 41641 open for direct WireGuard (Headscale advertises this; nodes may also use DERP relays).
  4. TCP 443 (or 8080 behind a reverse proxy) for the Headscale API and client registration.
  5. Docker Engine 24+ or a direct binary install—Docker is easier to upgrade.

If you are provisioning the host itself, our domain registration and hosting service covers DNS and VPS setup for Nepal-based teams who prefer local billing in NPR.

Minimum Software Versions

Run Headscale on Ubuntu 22.04 or 24.04 with Docker 24+. The Headscale container image tracks releases on GitHub; pin a specific tag in production rather than using latest. Client-side, install the current Tailscale package from Tailscale's official repo—the client version should be within the compatibility range noted in the Headscale release notes.

How Do You Install and Configure Headscale on Ubuntu?

The fastest path is Docker Compose with a bind-mounted config directory. Create a dedicated user, a config folder, and a persistent data volume for SQLite or PostgreSQL connection strings.

Step 1: Create the Directory Layout

sudo mkdir -p /opt/headscale/{config,data}
sudo chown -R $USER:$USER /opt/headscale
cd /opt/headscale

Step 2: Write config.yaml

The config file defines your server URL, listen address, database, and DNS settings. Adjust the server URL to match your public hostname—clients embed this value during login.

server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090

ip_prefixes:
  - 100.64.0.0/10

derp:
  server:
    enabled: false
  urls:
    - https://controlplane.tailscale.com/derpmap/default.json

database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

dns:
  override_local_dns: true
  nameservers:
    global:
      - 1.1.1.1
      - 8.8.8.8

log:
  level: info

Step 3: Docker Compose File

services:
  headscale:
    image: headscale/headscale:0.23
    container_name: headscale
    restart: unless-stopped
    command: headscale serve
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    ports:
      - "127.0.0.1:8080:8080"

Do not expose port 8080 publicly without TLS. Put Caddy or Nginx in front:

headscale.example.com {
    reverse_proxy localhost:8080
}

Start the stack with docker compose up -d. Verify with curl -I https://headscale.example.com/health—you should get HTTP 200.

Headscale Deployment Workflow1. DNS + TLS2. Config3. Docker4. ACL fileHeadscale Running on HTTPSCreate namespace, generate pre-auth keyRegister Laptoptailscale up --login-server=...Register Serverheadscale nodes listMesh Activeping 100.x.x.x works
Install Headscale, configure TLS and ACLs, then register Tailscale clients against your login server URL.

How Do You Register Tailscale Clients with Your Headscale Server?

Each logical group of machines in Headscale is called a user (formerly namespace). Create one per team or environment—production, staging, developers.

Create a User and Pre-Auth Key

docker exec headscale headscale users create production
docker exec headscale headscale preauthkeys create --user production --reusable --expiration 24h

Copy the generated key. On each machine, install Tailscale, then point the client at your server:

sudo tailscale up \
  --login-server=https://headscale.example.com \
  --authkey=tskey-auth-xxxxxxxx \
  --accept-routes \
  --accept-dns=false

On Linux servers you typically want --accept-dns=false so Headscale does not override /etc/resolv.conf on production boxes. Workstations can accept DNS if you configure MagicDNS equivalents in Headscale.

Approve Nodes (If Not Using Pre-Auth Keys)

Interactive registration generates a URL the user opens in a browser. Headscale prints a pending node; an admin approves it:

docker exec headscale headscale nodes list
docker exec headscale headscale nodes register --user production --key NODEKEY

Confirm connectivity with tailscale ping target-hostname or by pinging the assigned 100.x address from another node.

Subnet Routers and Exit Nodes

To reach an entire LAN—common when you self-host services like those described in our MinIO self-hosted S3 storage guide—advertise routes from one node:

sudo tailscale up \
  --login-server=https://headscale.example.com \
  --advertise-routes=192.168.1.0/24 \
  --authkey=tskey-auth-xxxxxxxx

Enable the route in Headscale:

docker exec headscale headscale routes list
docker exec headscale headscale routes enable --route-id 1

For projects like Adventure Third Pole Trek, where booking staff need VPN access to a staging Laravel app, subnet routing beats opening MySQL or Redis ports to the world.

How Do You Write Headscale ACL Policies for Production?

ACLs live in a HuJSON file—JSON with comments—mounted into the Headscale config directory. Start restrictive: deny by default, allow only what each group needs.

{
  "groups": {
    "group:admins": ["admin@example.com"],
    "group:devs": ["dev1@example.com", "dev2@example.com"]
  },
  "acls": [
    {
      "action": "accept",
      "src": ["group:admins"],
      "dst": ["*:*"]
    },
    {
      "action": "accept",
      "src": ["group:devs"],
      "dst": ["tag:staging:*"]
    }
  ],
  "tagOwners": {
    "tag:staging": ["group:admins"]
  }
}

Reload policy after edits:

docker exec headscale headscale policy check --file /etc/headscale/acl.hujson
docker exec headscale headscale policy set --file /etc/headscale/acl.hujson

Tag nodes during registration or afterward:

docker exec headscale headscale nodes tag --identifier 3 --tags tag:staging

Store ACL files in Git. Treat them like firewall rules—review changes, test in staging, then apply. A JSON formatter helps validate syntax before you push policy to production.

ACL Policy Enforcement FlowGit Repoacl.hujson trackedPolicy Checkheadscale policy checkHeadscaledistributes to nodesDenied Trafficblocked at WireGuardAllowed TrafficSSH, HTTP, DB portsPrinciple: default deny, explicit allow per group and tagReview ACL diffs in CI before applying to production Headscale
Track Headscale ACL files in Git, validate with policy check, then distribute enforced rules to every mesh node.

What Production Operations Keep Headscale Reliable?

Running the control plane is the easy part. Keeping it reliable for eighteen months is where teams stumble. These habits come from maintaining production Linux hosts alongside application stacks.

Back Up the Database

SQLite lives in your data volume. Schedule nightly copies:

0 2 * * * cp /opt/headscale/data/db.sqlite /backups/headscale-$(date +\%F).sqlite

For PostgreSQL, use pg_dump and test restores quarterly. Losing the database does not kill existing WireGuard tunnels immediately, but new nodes cannot register and ACL updates stop propagating.

Monitor TLS Expiry and Service Health

Caddy renews automatically; Certbot needs a systemd timer. Add an HTTP check against /health from an external monitor. If Headscale is down, existing peers keep talking, but policy changes stall.

Upgrade Strategy

Read Headscale release notes before upgrading—breaking changes to config keys happen. Pin image tags, bump one minor version at a time, and keep a snapshot of the data directory. Roll back by reverting the Compose tag and restarting.

Logging and Debugging

When a node shows Logged out or cannot register, check three places: Headscale logs (docker logs headscale), client logs (journalctl -u tailscaled), and whether the server's TLS certificate matches server_url exactly. A hostname mismatch produces cryptic client errors that look like network failures.

Pair Headscale with other self-hosted tooling only when it simplifies ops. Our n8n self-hosted workflow automation and self-hosted CI runners guides follow the same pattern: one VPS, Docker Compose, strict firewall, off-site backups.

Headscale Production GotchasTLS / server_url MismatchClient rejects login server certNo Database BackupsNode registry lost on disk failureOverly Permissive ACLsEvery node reaches every portPort 8080 Exposed RawNo reverse proxy or TLSFix: Caddy proxy, nightly DB backup, Git-tracked ACLsTest restore and ping after every upgrade
Avoid common Headscale failures: match TLS to server_url, back up the database, and never expose the API without HTTPS.

Security Hardening Checklist

  • Restrict Headscale admin CLI access to operators with SSH keys—no shared root passwords.
  • Use short-lived pre-auth keys for automated server provisioning; rotate after CI runs.
  • Enable UFW: allow 22 from your office IP, 443 public, 41641/udp public.
  • Generate strong keys with a password generator for any OIDC client secrets.
  • Audit registered nodes monthly; remove laptops belonging to former staff immediately.

If you prefer not to operate the control plane yourself, support and maintenance services can cover Headscale upgrades, backup verification, and ACL reviews alongside your application stack.

Key Takeaways

  • Headscale replaces Tailscale's cloud control plane—you still use the official Tailscale client for WireGuard tunnels.
  • Pin Docker image tags, terminate TLS at Caddy or Nginx, and never expose Headscale HTTP directly to the internet.
  • Create per-environment users, register nodes with pre-auth keys, and verify with tailscale ping.
  • Store ACL HuJSON in Git, default deny, and tag servers by role (tag:staging, tag:production).
  • Back up the SQLite or PostgreSQL database nightly—a control-plane loss blocks new registrations and policy updates.
  • Subnet routers let you reach whole LANs without opening public ports on internal services.

People Also Ask

Does Headscale work with the official Tailscale client?

Yes. Install Tailscale from the vendor repository on each device. Pass --login-server=https://your-headscale-domain during tailscale up. Do not use forked clients—the official build expects a coordination server speaking the Tailscale protocol, which Headscale implements.

Can Headscale replace a VPN appliance for remote workers?

For most small and mid-size teams, yes. Workers install Tailscale, join your Headscale user group, and reach office subnets through an advertised route. You still need a machine on the LAN acting as subnet router. Traditional IPsec appliances offer more legacy protocol support; Headscale wins on setup speed and per-device management.

Is Headscale free to use in production?

Headscale is open source under the BSD licence. Your costs are the VPS, domain, and operator time—not per-seat licensing. Budget roughly Rs 1,000–2,500 per month (~USD 8–20) for a control-plane server in Nepal or Singapore region hosting, plus the time to patch and back it up.

What happens if the Headscale server goes offline?

Existing WireGuard peer connections keep working until keys expire or nodes restart. New devices cannot register, ACL changes do not propagate, and nodes that reboot may fail to re-authenticate. Run Headscale on a monitored VPS with automated restarts and tested backups to limit outage impact.

Run Your Own Mesh VPN Control Plane

You now have a complete path to Self-Host Tailscale with Headscale: provision a small Ubuntu box, deploy Headscale behind TLS, register clients with pre-auth keys, and enforce least-privilege ACLs from Git. The mesh gives your team SSH, database, and admin-panel access without punching holes in public firewalls—the same pattern I use alongside Deployer releases on shared EC2 hosts and Laravel on AWS EC2 stacks. If you want the VPN layer designed, hardened, and maintained alongside your web apps, contact us or explore custom software development and web development services. Read more self-hosting guides on the blog, browse Notary Kathmandu and other portfolio projects running on production Linux infrastructure, or learn about the author on the about me page.

Frequently Asked Questions

Headscale is an open-source, self-hosted replacement for Tailscale's cloud control plane. It does not replace the Tailscale client on your laptops and servers. Headscale coordinates who is allowed to talk to whom by distributing node keys, ACL policies, and DNS settings. The official Tailscale client still handles WireGuard tunnels, NAT traversal, and peer discovery. When you self-host Tailscale with Headscale, you get the same mesh VPN behaviour while keeping the network map and policy definitions on infrastructure you control.

Headscale is open source under the BSD licence with no per-seat fees. Your costs are a small VPS, domain, and operator time. Budget roughly Rs 800–2,500 per month (~USD 6–18) for the control-plane server, or Rs 1,000–2,500 (~USD 8–20) in Nepal or Singapore region hosting, plus time to patch and back it up. That is predictable server billing instead of Tailscale SaaS per-user pricing.

Yes. Install the current Tailscale package from Tailscale's official repo on each device. During registration, pass --login-server=https://your-headscale-domain with tailscale up. Do not use forked clients—the official build expects a coordination server speaking the Tailscale protocol, which Headscale implements. Client behaviour follows Tailscale's installation guides; server behaviour follows the Headscale project documentation.

Headscale makes sense when you have five to five hundred nodes and want predictable VPS cost instead of per-seat SaaS pricing. Choose it when compliance or client policy requires the network map and ACL definitions on infrastructure you control, when you already run Ubuntu servers, or when you need custom OIDC without Tailscale's enterprise tier. Stay on hosted Tailscale if your team is small, billing is not a concern, and you want MagicDNS, subnet routing UI, and support without operational overhead.

Headscale is lightweight—a 1 vCPU, 1 GB RAM Ubuntu 24.04 VPS handles dozens of nodes comfortably. You need a public hostname with an A record, TLS via Caddy or Certbot, Docker Engine 24+ or a binary install, UDP port 41641 open for WireGuard, and TCP 443 or 8080 behind a reverse proxy for the API. Use SQLite for small deployments; switch to PostgreSQL when you expect hundreds of nodes or want easier backup tooling. Ubuntu 22.04 or 24.04 both work.

Create /opt/headscale with config and data directories, write config.yaml defining server_url, listen address, ip_prefixes, database, and DNS settings, then deploy headscale/headscale with a pinned tag such as 0.23 via Docker Compose binding those volumes. Bind port 8080 to localhost only and put Caddy or Nginx in front for TLS—never expose Headscale HTTP directly to the internet. Start with docker compose up -d and verify with curl -I https://headscale.example.com/health for HTTP 200.

Create a Headscale user per team or environment, then generate a reusable pre-auth key with a short expiration. On each machine, install Tailscale and run tailscale up with --login-server pointing at your Headscale URL, --authkey, and typically --accept-routes. For interactive registration, approve pending nodes via headscale nodes list and headscale nodes register. Confirm connectivity with tailscale ping or by pinging the assigned 100.x address from another node.

Open UDP port 41641 for direct WireGuard connections—Headscale advertises this and nodes may also use DERP relays from Tailscale's default map when local DERP is disabled. Expose TCP 443 publicly for the Headscale API and client registration, or bind 8080 locally and terminate TLS at a reverse proxy. Restrict admin access with UFW: allow SSH from trusted IPs, 443 publicly, and 41641/udp publicly. Match your TLS certificate hostname exactly to server_url in config.yaml.

Existing WireGuard peer connections keep working until keys expire or nodes restart. New devices cannot register, ACL changes do not propagate, and nodes that reboot may fail to re-authenticate. Losing the database does not kill active tunnels immediately, but new node registration and policy updates stop. Run Headscale on a monitored VPS with automated restarts, external /health checks, and tested nightly database backups to limit outage impact.

ACLs live in a HuJSON file mounted into the Headscale config directory. Define groups, tagOwners, and acls starting restrictive—deny by default, allow only what each group needs. Tag nodes by role such as tag:staging or tag:production. Validate syntax with headscale policy check, then apply with headscale policy set. Store ACL files in Git, review changes like firewall rules, test in staging, then push to production. Reload after every edit.

On Linux servers you typically want --accept-dns=false during tailscale up so Headscale does not override /etc/resolv.conf on production boxes. Workstations can accept DNS if you configure MagicDNS equivalents in Headscale's dns settings. The article's sample config sets override_local_dns true with global nameservers 1.1.1.1 and 8.8.8.8 on the control plane, but production servers often need local resolver behaviour unchanged to avoid breaking application lookups.

From a machine on the target LAN, run tailscale up with --advertise-routes for the subnet, for example 192.168.1.0/24, plus your --login-server and auth key. List routes in Headscale with headscale routes list, then enable the approved route with headscale routes enable --route-id. This lets mesh members reach entire office or staging LANs without opening MySQL, Redis, or admin ports to the public internet. Full subnet router and exit node support is available with manual configuration.

SQLite is fine for small deployments and lives in your bind-mounted data volume at /var/lib/headscale/db.sqlite. Switch to PostgreSQL when you expect hundreds of nodes or want easier backup tooling with pg_dump and quarterly restore tests. Either way, schedule nightly backups—a control-plane loss blocks new registrations and ACL propagation even though existing WireGuard tunnels may continue briefly. Pin database connection settings in config.yaml and keep snapshots before upgrades.

Back up the SQLite or PostgreSQL database nightly with cron or pg_dump and test restores quarterly. Monitor TLS expiry—Caddy renews automatically while Certbot needs a systemd timer—and add external HTTP checks against /health. Read Headscale release notes before upgrades, pin Docker image tags, bump one minor version at a time, and snapshot the data directory before changes. When nodes fail to register, check Headscale logs, tailscaled via journalctl, and verify TLS matches server_url exactly.

Restrict Headscale admin CLI access to operators with SSH keys—no shared root passwords. Use short-lived reusable pre-auth keys for automated provisioning and rotate after CI runs. Enable UFW with minimal open ports, generate strong OIDC client secrets, and audit registered nodes monthly—remove laptops belonging to former staff immediately. Never expose the Headscale API without HTTPS. Treat ACL HuJSON like firewall rules in Git with review before production deployment.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: