Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Headscale Ubuntu
GUIDEInstall Guides

How to Install Headscale on Ubuntu 24.04 VPS — Self-Hosted Tailscale Coordination Server

29 min read

How to Install Headscale on Ubuntu 24.04 VPS — Self-Hosted Tailscale Coordination Server

Headscale is an open-source, self-hosted implementation of the Tailscale control server. It lets you run a fully private mesh VPN built on WireGuard without relying on Tailscale's SaaS coordination layer. This tutorial walks you through installing Headscale on an Ubuntu 24.04 VPS, configuring TLS, wiring up pre-auth keys, writing ACLs, deploying headscale-ui, configuring an embedded DERP relay, and fronting the whole thing with Nginx for HTTPS and gRPC.

Want a private WireGuard mesh without Tailscale's per-user pricing? Deploy Headscale on a CloudCore Starter VPS for EUR 7.99/month and coordinate unlimited nodes across your infrastructure.

Table of Contents

  • What is Headscale?
  • Why Self-Host a Coordination Server?
  • Headscale vs. Tailscale SaaS vs. NetBird
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Install Headscale from the .deb Package
  • Step 3: Configure config.yaml
  • Step 4: Create the systemd Service
  • Step 5: Create a User and Pre-Auth Key
  • Step 6: Register Tailscale Clients
  • Step 7: Write and Apply ACLs
  • Step 8: Configure the Embedded DERP Relay
  • Step 9: Nginx Reverse Proxy for HTTPS and gRPC
  • Step 10: Deploy headscale-ui
  • Troubleshooting
  • FAQ
  • Next Steps
  • What is Headscale?

    Headscale is an open-source re-implementation of Tailscale's coordination server, written in Go and maintained primarily by Juan Font. It speaks the same protocol as the hosted Tailscale control plane, which means the official Tailscale clients on Linux, macOS, Windows, iOS, and Android connect to it without any modification. The source lives on GitHub at juanfont/headscale under the BSD-3-Clause license.

    Under the hood Headscale is a coordination and key-exchange service. WireGuard itself does the actual encrypted tunnel work on each node; Headscale's job is to hold the node registry, distribute public keys, publish the network map (which peers exist, which IPs they have, which routes they advertise), enforce access control, and optionally relay traffic through a DERP server when direct peer-to-peer NAT traversal fails. No user traffic is routed through Headscale in the common case — it is a control plane, not a data plane.

    Headscale supports the features most users actually rely on from Tailscale: pre-auth keys for unattended node enrollment, OIDC single sign-on (Keycloak, Authentik, Google, GitHub), subnet routing so one node can advertise a CIDR from an on-premise LAN, exit nodes to route all client traffic through a chosen peer, MagicDNS with .tailnet resolution, tagged nodes for role-based ACLs, and HuJSON ACLs that are byte-compatible with Tailscale's own policy format. Features currently not implemented include Tailscale SSH, Funnel (public ingress), and Taildrop.

    Why Self-Host a Coordination Server?

    Running your own Headscale server alongside the WireGuard clients gives you control and economics that the hosted service cannot match:

    • No per-user pricing -- Tailscale charges per user and caps devices per plan on its free tier. A Headscale VPS costs a flat EUR 7.99/month regardless of whether you have three nodes or three hundred.
    • Full data sovereignty -- Your node registry, key material, and ACL policy stay on your server in an EU jurisdiction of your choice. Nothing is shared with a third-party SaaS.
    • Custom ACL logic -- You can iterate on the ACL file, reload it live, and integrate policy changes into your own CI/CD pipeline. No waiting on upstream UI changes.
    • Own your DERP -- Running your own DERP relay means fallback NAT-traversal traffic stays inside your infrastructure and on a VPS with known bandwidth, not on whichever Tailscale-operated relay is closest.
    • OIDC freedom -- Headscale integrates with any OIDC provider. Plug in your existing Keycloak, Authentik, or Zitadel instance instead of creating yet another identity silo.
    • No vendor lock-in -- Headscale uses a Postgres or SQLite database you control. Migration or backup is a file copy, not an export ticket.
    • Air-gapped deployments -- Headscale can run entirely on a private network with no outbound internet, coordinating nodes in isolated lab or industrial environments.

    Headscale vs. Tailscale SaaS vs. NetBird

    Picking the right mesh VPN depends on how much operational surface you want to own. Here is an honest comparison of the three most common options.

    FeatureHeadscale (self-hosted)Tailscale SaaSNetBird (self-hosted)
    Underlying protocolWireGuardWireGuardWireGuard
    Coordination serverYou run itTailscale-hostedYou run it (or SaaS tier)
    Monthly cost (20 nodes)EUR 7.99 (VPS)~USD 60-120 (user-based)EUR 7.99 (VPS) or USD 0-9/user
    Per-user feesNoneYes, above free tierFree self-hosted, paid SaaS
    ACL formatHuJSON (Tailscale-compatible)HuJSONJSON policy
    OIDC SSOYesYesYes
    Web UICommunity (headscale-ui)OfficialOfficial
    Tailscale SSHNot yetYesN/A
    Funnel (public ingress)Not implementedYesVia peer routing
    DERP relayEmbedded or externalTailscale-operatedTURN via Coturn
    Mobile clientOfficial Tailscale appsOfficial Tailscale appsOfficial NetBird apps
    Data residencyYour VPSUSA (Tailscale)Your VPS or EU SaaS
    Best forTeams who already like Tailscale clients and want to drop the SaaS billSmall teams who value zero opsTeams who want a polished built-in UI and don't mind running the full stack
    If you are replacing a legacy OpenVPN deployment, all three options are dramatically simpler to operate. OpenVPN's certificate and routing complexity evaporates once you move to a WireGuard-based mesh.

    Headscale wins specifically when you already trust the Tailscale client ecosystem (mobile apps, Magic DNS, exit nodes) and just want to remove the hosted control plane from the equation.

    Prerequisites

    Before starting, make sure you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access.
    • SSH access to the server.
    • A public domain name pointed at the server's IPv4 (and ideally IPv6) address. Headscale requires a real TLS certificate — clients will not accept self-signed certs in production. In this guide we use headscale.example.com.
    • Ports 80 and 443 open on the firewall and reachable from the public internet, required for Let's Encrypt issuance and client connections.
    • Port 3478/udp open if you plan to run the embedded DERP relay (STUN probe).
    • At least 2 GB of RAM for up to a few hundred nodes; 4 GB is comfortable for production.
    Recommended Plan: CloudCore Starter
    >
    The Headscale daemon itself is extremely lightweight. Memory usage stays under 100 MB even with hundreds of nodes connected, and the SQLite database is a few MB. For the vast majority of deployments, the CloudCore Starter plan is the right fit:
    >
    - 2 vCPU cores
    - 4 GB RAM
    - 50 GB NVMe SSD
    - Unmetered bandwidth
    - EUR 7.99/month
    >
    For larger deployments (1,000+ nodes) or heavy DERP relay traffic, step up to CloudCore Professional at EUR 19.99/month (6 vCPU, 12 GB RAM, 100 GB NVMe) or CloudCore Business at EUR 29.99+/month (dedicated cores, 16-32 GB RAM).

    Connect to your server:

    bash
    ssh root@your-server-ip

    Step 1: Update System Packages

    Bring the system up to date before installing new services:

    bash
    sudo apt update && sudo apt upgrade -y

    Install the utilities we will need for the rest of the guide:

    bash
    sudo apt install -y curl wget gnupg ca-certificates ufw

    If the kernel was updated, reboot and reconnect:

    bash
    sudo reboot

    Set a hostname that matches the domain you will use (optional but clean):

    bash
    sudo hostnamectl set-hostname headscale

    Step 2: Install Headscale from the .deb Package

    The Headscale project publishes official .deb packages on its GitHub releases page, which is the simplest path on Ubuntu. Grab the latest stable release for amd64:

    bash
    HEADSCALE_VERSION="0.26.1"
    wget -O /tmp/headscale.deb \
      "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64.deb"

    Install the package:

    bash
    sudo dpkg -i /tmp/headscale.deb

    Expected output:

    text
    Selecting previously unselected package headscale.
    (Reading database ... 63214 files and directories currently installed.)
    Preparing to unpack /tmp/headscale.deb ...
    Unpacking headscale (0.26.1) ...
    Setting up headscale (0.26.1) ...
    Created symlink /etc/systemd/system/multi-user.target.wants/headscale.service → /lib/systemd/system/headscale.service.

    The package performs four actions:

  • Installs the headscale binary to /usr/bin/headscale.
  • Creates a system user named headscale that owns the service.
  • Creates /etc/headscale/ for configuration and /var/lib/headscale/ for state.
  • Installs a systemd unit at /lib/systemd/system/headscale.service but does not start it — we want to configure first.
  • Verify the binary:

    bash
    headscale version

    Expected output:

    text
    0.26.1

    Step 3: Configure config.yaml

    Headscale reads its configuration from /etc/headscale/config.yaml. The package installs a template; replace it with a production-ready version.

    Back up the template:

    bash
    sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.dist

    Create the new configuration:

    bash
    sudo tee /etc/headscale/config.yaml > /dev/null <<'EOF'
    

    The public URL clients will use to reach your control server.

    Must be HTTPS in production.

    server_url: https://headscale.example.com

    Address Headscale binds to locally. Nginx will forward to this.

    listen_addr: 127.0.0.1:8080

    Metrics endpoint (Prometheus-compatible). Bound to localhost only.

    metrics_listen_addr: 127.0.0.1:9090

    gRPC listener, used by the headscale CLI for remote admin.

    grpc_listen_addr: 127.0.0.1:50443 grpc_allow_insecure: false

    Private key used to sign node registrations.

    private_key_path: /var/lib/headscale/private.key

    Noise protocol key, required by Tailscale v2 clients.

    noise: private_key_path: /var/lib/headscale/noise_private.key

    The CIDR ranges assigned to nodes on the tailnet.

    prefixes: v4: 100.64.0.0/10 v6: fd7a:115c:a1e0::/48

    Database backend. SQLite is fine for most deployments up to ~1k nodes.

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

    For larger deployments, switch to Postgres:

    type: postgres

    postgres:

    host: 127.0.0.1

    port: 5432

    name: headscale

    user: headscale

    pass: changeme

    DERP configuration — we define our embedded relay in Step 8.

    derp: server: enabled: true region_id: 999 region_code: "local" region_name: "Self-Hosted DERP" stun_listen_addr: "0.0.0.0:3478" urls: - https://controlplane.tailscale.com/derpmap/default auto_update_enabled: true update_frequency: 24h

    Ephemeral node inactivity timeout.

    ephemeral_node_inactivity_timeout: 30m

    How long before an unused pre-auth key expires.

    node_update_check_interval: 10s

    Log level: trace | debug | info | warn | error

    log: level: info format: text

    DNS settings pushed to all clients.

    dns: magic_dns: true base_domain: tailnet.example.com nameservers: global: - 1.1.1.1 - 9.9.9.9 search_domains: []

    ACL policy file (created in Step 7).

    policy: mode: file path: /etc/headscale/acl.hujson

    Address used by clients that fall back to a DERP relay.

    unix_socket: /var/run/headscale/headscale.sock unix_socket_permission: "0770" EOF

    A few important notes on the fields:

    • server_url must match exactly what clients will use in tailscale up --login-server=.... Get this wrong and clients silently reject TLS certificates.
    • listen_addr: 127.0.0.1:8080 binds Headscale to localhost only. Nginx will terminate TLS and forward traffic in Step 9.
    • grpc_listen_addr: 127.0.0.1:50443 is the admin CLI endpoint. Nginx will also proxy this over HTTPS on port 443 so that headscale CLI calls from your laptop work.
    • prefixes.v4: 100.64.0.0/10 is the RFC 6598 carrier-grade NAT range Tailscale/Headscale use for overlay IPs. Do not change unless you know why.
    • dns.base_domain must be a domain different from server_url. Here server_url is headscale.example.com and the tailnet MagicDNS suffix is tailnet.example.com.
    Fix ownership on the config:

    bash
    sudo chown -R headscale:headscale /etc/headscale /var/lib/headscale
    sudo chmod 600 /etc/headscale/config.yaml

    Create the socket directory:

    bash
    sudo mkdir -p /var/run/headscale
    sudo chown headscale:headscale /var/run/headscale

    Step 4: Create the systemd Service

    The .deb installed a systemd unit, but we want to confirm its behaviour and add a tmpfiles rule so /var/run/headscale survives reboots.

    Inspect the installed service:

    bash
    cat /lib/systemd/system/headscale.service

    Create the tmpfiles rule so the socket directory is recreated on every boot:

    bash
    sudo tee /etc/tmpfiles.d/headscale.conf > /dev/null <<EOF
    d /var/run/headscale 0770 headscale headscale -
    EOF

    Apply it:

    bash
    sudo systemd-tmpfiles --create

    Enable and start the service:

    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now headscale

    Check status:

    bash
    sudo systemctl status headscale

    Expected output:

    text
    ● headscale.service - headscale coordination server for Tailscale
         Loaded: loaded (/lib/systemd/system/headscale.service; enabled; preset: enabled)
         Active: active (running) since Thu 2026-04-16 09:00:00 UTC; 3s ago
       Main PID: 1842 (headscale)
          Tasks: 8 (limit: 4617)
         Memory: 38.6M
            CPU: 156ms
         CGroup: /system.slice/headscale.service
                 └─1842 /usr/bin/headscale serve

    Follow logs to confirm it booted cleanly:

    bash
    sudo journalctl -u headscale -n 40 --no-pager

    You should see lines like listening and serving HTTP on 127.0.0.1:8080 and DERP server started on :3478.

    Step 5: Create a User and Pre-Auth Key

    Headscale organises nodes under users (analogous to Tailscale's user identity). Create one for your first batch of machines:

    bash
    sudo headscale users create ops

    Expected output:

    text
    User created
    ID | Name | Created
    1  | ops  | 2026-04-16 09:05:00

    List users:

    bash
    sudo headscale users list

    Pre-auth keys let you enroll a node in one non-interactive step, which is how we will register servers, CI runners, and Kubernetes pods. Generate a reusable, ephemeral key valid for 24 hours:

    bash
    sudo headscale preauthkeys create --user ops --reusable --expiration 24h

    Expected output:

    text
    ced26c...truncated...a6b3f

    Variant keys you may also want:

    • One-shot key (single node, can't be reused):
    bash
    sudo headscale preauthkeys create --user ops --expiration 1h
    • Ephemeral key (node auto-removed when it disconnects, ideal for CI):
    bash
    sudo headscale preauthkeys create --user ops --ephemeral --expiration 24h
    • Tagged key (key bakes in ACL tags — see Step 7):
    bash
    sudo headscale preauthkeys create --user ops --reusable --expiration 24h \
        --tags tag:server,tag:prod

    List keys:

    bash
    sudo headscale preauthkeys list --user ops

    Step 6: Register Tailscale Clients

    Before proceeding, complete Step 9 (Nginx + TLS) on the Headscale server so that https://headscale.example.com resolves and serves a valid certificate. Then return to this step to enroll nodes.

    Linux Node

    Install Tailscale using the official package repo:

    bash
    curl -fsSL https://tailscale.com/install.sh | sh

    Enroll the node against your Headscale server with the pre-auth key:

    bash
    sudo tailscale up \
      --login-server=https://headscale.example.com \
      --authkey=ced26c...your-key-here...a6b3f \
      --accept-routes \
      --accept-dns

    Expected output:

    text
    Success.

    Verify the node is registered from the Headscale server:

    bash
    sudo headscale nodes list

    Expected output:

    text
    ID | Hostname       | Name         | MachineKey | NodeKey    | User | IP addresses               | Ephemeral | Last seen           | Online
    1  | web-01         | web-01       | [abc...]   | [def...]   | ops  | 100.64.0.1, fd7a:115c:...  | false     | 2026-04-16 09:20:00 | online

    macOS / Windows Node

    The Tailscale Mac and Windows apps accept a custom login server but it is buried in the settings. A cleaner approach is to set the login server via CLI after installing the app:

    macOS (with the CLI enabled):

    bash
    tailscale login --login-server=https://headscale.example.com

    Windows (PowerShell, requires the open-source Tailscale client or tailscale.exe on PATH):

    powershell
    tailscale login --login-server=https://headscale.example.com

    Both platforms will open a browser window showing a headscale registration command. Copy the device key from the URL and run on the Headscale server:

    bash
    sudo headscale nodes register --user ops --key nodekey:abc123...

    Mobile

    The official Tailscale iOS and Android apps require three taps from the login screen to enter a custom coordination server URL. Open the app, tap the settings gear, tap "Accounts", tap "Use custom coordination server", and enter https://headscale.example.com.

    Step 7: Write and Apply ACLs

    Headscale uses Tailscale's HuJSON ACL format. Create an initial policy that defines tags, groups, and explicit allow rules:

    bash
    sudo tee /etc/headscale/acl.hujson > /dev/null <<'EOF'
    {
      // Groups gather users under a label.
      "groups": {
        "group:admins": ["[email protected]", "[email protected]"],
        "group:devs":   ["[email protected]"]
      },

    // Tag owners declare which users may apply each tag to a node. "tagOwners": { "tag:server": ["group:admins"], "tag:prod": ["group:admins"], "tag:ci": ["group:admins"], "tag:dev": ["group:admins", "group:devs"] },

    // ACLs are evaluated top-to-bottom. First match wins. "acls": [ // Admins have unrestricted access to everything. { "action": "accept", "src": ["group:admins"], "dst": [":"] },

    // Devs can reach dev-tagged nodes on any port. { "action": "accept", "src": ["group:devs"], "dst": ["tag:dev:*"] },

    // CI runners can reach prod nodes only on SSH and HTTPS. { "action": "accept", "src": ["tag:ci"], "dst": ["tag:prod:22,443"] },

    // All tagged servers can talk to each other on any port // (internal service-to-service traffic). { "action": "accept", "src": ["tag:server"], "dst": ["tag:server:*"] } ],

    // SSH rules (informational — Tailscale SSH is not enforced by Headscale yet). "ssh": [ { "action": "accept", "src": ["group:admins"], "dst": ["tag:server"], "users": ["root", "ubuntu"] } ] } EOF

    Apply and validate:

    bash
    sudo chown headscale:headscale /etc/headscale/acl.hujson
    sudo headscale policy check --file /etc/headscale/acl.hujson

    Expected output:

    text
    Policy is valid

    Reload the policy live (no service restart required):

    bash
    sudo headscale policy set --file /etc/headscale/acl.hujson

    Apply tags to an existing node:

    bash
    sudo headscale nodes tag -i 1 -t tag:server,tag:prod

    Step 8: Configure the Embedded DERP Relay

    DERP (Designated Encrypted Relay for Packets) is Tailscale's fallback when two peers cannot establish a direct UDP connection through NAT. Headscale can point clients at Tailscale's public DERP mesh (which the urls list in config.yaml does by default), run its own embedded DERP server, or both.

    The derp.server block we added in Step 3 already enabled the embedded relay on UDP port 3478 (STUN) and the HTTPS listener (on the same port as Headscale itself). Open the firewall:

    bash
    sudo ufw allow 3478/udp comment 'DERP STUN'
    sudo ufw allow 80/tcp comment 'HTTP (Let''s Encrypt)'
    sudo ufw allow 443/tcp comment 'HTTPS (Headscale + DERP)'
    sudo ufw allow 22/tcp comment 'SSH'
    sudo ufw enable

    Confirm the relay is reachable:

    bash
    curl -s https://headscale.example.com/derp/probe

    Expected output:

    text
    DERP server is running

    From any client, inspect the DERP status:

    bash
    tailscale netcheck

    Expected output (abbreviated):

    text
    Report:
        * Your DERP region: 999 (Self-Hosted DERP)
        * DERP latency:
            - 999: 12ms (Self-Hosted DERP)

    If you want to disable the public Tailscale DERP mesh entirely and use only your own relay, remove the derp.urls list from config.yaml and reload:

    yaml
    derp:
      server:
        enabled: true
        region_id: 999
        ...
      # urls: []   # no external DERP
      auto_update_enabled: false

    Restart:

    bash
    sudo systemctl restart headscale

    Step 9: Nginx Reverse Proxy for HTTPS and gRPC

    Headscale does not terminate TLS itself in production deployments — Nginx (or Caddy) handles that, then forwards to the local listener. We also need Nginx to forward gRPC on a separate HTTPS vhost so that the headscale CLI can be used remotely.

    Install Nginx and Certbot:

    bash
    sudo apt install -y nginx certbot python3-certbot-nginx

    Create the main HTTPS vhost for the coordination server:

    bash
    sudo tee /etc/nginx/sites-available/headscale > /dev/null <<'EOF'
    

    HTTP -> HTTPS redirect.

    server { listen 80; listen [::]:80; server_name headscale.example.com;

    location /.well-known/acme-challenge/ { root /var/www/html; } location / { return 301 https://$host$request_uri; } }

    Main coordination server (Tailscale client traffic).

    server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name headscale.example.com;

    ssl_certificate /etc/letsencrypt/live/headscale.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/headscale.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; add_header X-Content-Type-Options nosniff;

    # Clients hold long-polling connections open. Raise timeouts. keepalive_timeout 900s; client_max_body_size 50m;

    location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1;

    proxy_set_header Host $host; 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;

    # WebSockets are used for noise streaming. proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";

    proxy_buffering off; proxy_read_timeout 900s; proxy_send_timeout 900s; } } EOF

    Create a second vhost for the gRPC admin endpoint on a different hostname (headscale-grpc.example.com):

    bash
    sudo tee /etc/nginx/sites-available/headscale-grpc > /dev/null <<'EOF'
    server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name headscale-grpc.example.com;

    ssl_certificate /etc/letsencrypt/live/headscale-grpc.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/headscale-grpc.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;

    # gRPC requires HTTP/2 end to end. location / { grpc_pass grpcs://127.0.0.1:50443; grpc_read_timeout 900s; grpc_send_timeout 900s;

    error_page 502 = /error502grpc; }

    location = /error502grpc { internal; default_type application/grpc; add_header grpc-status 14; add_header content-length 0; return 204; } } EOF

    Note the use of grpc_pass grpcs://.... Headscale's gRPC listener is TLS itself when grpc_allow_insecure: false — which means Nginx must terminate the public HTTPS and then re-encrypt to the local listener. For simplicity you can set grpc_allow_insecure: true in config.yaml (since it is bound to 127.0.0.1) and switch to grpc_pass grpc://... without TLS on the backhaul.

    Enable both sites:

    bash
    sudo ln -s /etc/nginx/sites-available/headscale      /etc/nginx/sites-enabled/
    sudo ln -s /etc/nginx/sites-available/headscale-grpc /etc/nginx/sites-enabled/
    sudo nginx -t

    Issue certificates:

    bash
    sudo certbot --nginx \
      -d headscale.example.com \
      -d headscale-grpc.example.com \
      --non-interactive --agree-tos --email [email protected]

    Reload:

    bash
    sudo systemctl reload nginx

    Verify end-to-end from your laptop:

    bash
    curl -v https://headscale.example.com/health

    Expected output:

    text
    {"status":"pass"}

    Use the CLI remotely via the gRPC endpoint. On your laptop, install the headscale binary and create ~/.config/headscale/config.yaml:

    yaml
    cli:
      address: headscale-grpc.example.com:443
      api_key: <generate with: headscale apikeys create>
      insecure: false

    Generate and copy the API key on the server:

    bash
    sudo headscale apikeys create --expiration 90d

    Paste the returned key into the laptop config. Test:

    bash
    headscale nodes list

    Step 10: Deploy headscale-ui

    Headscale ships without a web UI — admin is CLI-first. The community project headscale-ui provides a lightweight React frontend that talks to the Headscale gRPC API.

    Run it in Docker alongside Headscale:

    bash
    sudo apt install -y docker.io
    sudo systemctl enable --now docker

    sudo docker run -d \ --name headscale-ui \ --restart unless-stopped \ -p 127.0.0.1:8443:443 \ ghcr.io/gurucomputing/headscale-ui:latest

    Add a third Nginx vhost to serve the UI on ui.headscale.example.com:

    bash
    sudo tee /etc/nginx/sites-available/headscale-ui > /dev/null <<'EOF'
    server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name ui.headscale.example.com;

    ssl_certificate /etc/letsencrypt/live/ui.headscale.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ui.headscale.example.com/privkey.pem;

    location / { proxy_pass https://127.0.0.1:8443; proxy_ssl_verify off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } } EOF

    sudo ln -s /etc/nginx/sites-available/headscale-ui /etc/nginx/sites-enabled/ sudo certbot --nginx -d ui.headscale.example.com --non-interactive --agree-tos --email [email protected] sudo systemctl reload nginx

    Browse to https://ui.headscale.example.com. In the Settings panel, set the API URL to https://headscale-grpc.example.com and paste the API key you generated in Step 9. You now have a browser-based view of users, nodes, pre-auth keys, and (read-only) the current ACL policy.

    Troubleshooting

    ProblemCauseSolution
    Client reports control server error: self signed certificateserver_url doesn't match the TLS cert, or Let's Encrypt didn't issueCheck openssl s_client -connect headscale.example.com:443 and curl -I https://headscale.example.com/health. Re-run Certbot.
    tailscale up hangs on "Waiting for login"Nginx is blocking WebSocket upgrade or bufferingEnsure the Upgrade/Connection headers and proxy_buffering off are set in the vhost.
    Nodes show as offline but can ping each otherNormal — "offline" reflects whether the node has an active connection to Headscale, not peer reachabilityRun tailscale status on the node to see real peer state.
    headscale nodes list from laptop returns rpc error: code = UnauthenticatedExpired or missing API keyRegenerate: sudo headscale apikeys create --expiration 90d and update local config.
    DERP fallback not working (tailscale netcheck shows "no working DERP")UDP 3478 blocked or embedded DERP disabledCheck ufw status for 3478/udp allow. Confirm derp.server.enabled: true in config.yaml.
    High CPU on server after ACL changeHeadscale recomputes the full network map for every node on policy reloadExpected on large tailnets — typically settles within 30 seconds for <500 nodes.
    tailscale up error: invalid key: no matching preauth keyKey expired or not reusable and already consumedCreate a fresh key: sudo headscale preauthkeys create --user ops --reusable --expiration 24h.
    MagicDNS names not resolvingbase_domain set to the same as server_urlThey must differ. server_url: headscale.example.com + base_domain: tailnet.example.com is correct.

    Viewing Logs

    Stream the Headscale log in real time:

    bash
    sudo journalctl -u headscale -f

    Bump the verbosity temporarily:

    yaml
    log:
      level: debug

    Then:

    bash
    sudo systemctl restart headscale

    Nginx access and error logs are the other place to look when clients cannot connect:

    bash
    sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log

    FAQ

    Is Headscale compatible with the official Tailscale clients?

    Yes. Headscale speaks the same control protocol that the Tailscale-hosted service does, so the unmodified Tailscale clients on Linux, macOS, Windows, iOS, and Android connect to it by pointing at --login-server=https://your-headscale.example.com. You do not need a custom client, and you do not lose MagicDNS, exit nodes, or subnet routes.

    How many nodes can a single Headscale VPS handle?

    The Headscale daemon is remarkably efficient because it holds almost no state at runtime — node registrations live in the database and the process itself is mostly a pub/sub map distributor. On a CloudCore Starter (2 vCPU, 4 GB RAM) we comfortably run up to ~500 nodes with SQLite. At 1,000-5,000 nodes we recommend moving to Postgres and stepping up to CloudCore Professional (EUR 19.99/month) to absorb ACL recomputation spikes. Beyond 10,000 nodes, look at running multiple Headscale instances behind a load balancer with a shared Postgres — though the maintainers recommend considering Tailscale SaaS at that scale.

    Do I need to run my own DERP relay?

    Not strictly. By default (and with our config.yaml from Step 3) Headscale pulls Tailscale's public DERP map every 24 hours, so your clients automatically use Tailscale's globally distributed relay network as fallback. Running your own embedded DERP on the same VPS is useful for three reasons: (1) you keep fallback traffic inside your known infrastructure, (2) you often get lower latency for clients physically near the server, and (3) you remove the dependency on Tailscale's network entirely, enabling truly air-gapped deployments. The tradeoff is bandwidth: if many of your nodes are behind strict NAT and fail back to DERP, your VPS carries that traffic.

    Can I migrate from Tailscale SaaS to Headscale without downtime?

    Partially. The node identities (device keys) are not transferable — each device has to re-authenticate against the new coordination server. The practical migration is: stand up Headscale in parallel, create pre-auth keys, roll through your nodes calling tailscale logout && tailscale up --login-server=..., and finally turn off the Tailscale SaaS account. Users will see a brief per-node disconnect during the re-auth. For most small-to-medium networks this is a 30-60 minute evening job.

    Is Headscale production-ready?

    For most use cases, yes. Headscale is used in production by thousands of organisations and the project has a regular release cadence with an active maintainer community. The caveats: the gRPC API surface sometimes changes between minor versions (read the release notes before upgrading), and certain Tailscale features — Tailscale SSH enforcement, Funnel, Taildrop — are not implemented. If you rely on those, you'll want Tailscale SaaS. For pure WireGuard-mesh-with-ACLs-and-DNS, Headscale is solid.

    How do I back up Headscale?

    Three files are sufficient for full disaster recovery: /etc/headscale/config.yaml, /etc/headscale/acl.hujson, and /var/lib/headscale/db.sqlite (plus the private key files in /var/lib/headscale/). Snapshot them nightly to off-VPS storage. If you are using Postgres, add pg_dump -U headscale headscale > headscale.sql to the backup script. Restore is a file copy and systemctl restart headscale. We recommend automating with a simple cron job and shipping to S3-compatible object storage.

    Next Steps

    With Headscale running, here are recommended follow-ups:

    • Wire in OIDC SSO -- Replace local users with your existing identity provider. Headscale supports Keycloak, Authentik, Zitadel, Google Workspace, and any generic OIDC server. Add the oidc block to config.yaml and users log in with their corporate account instead of pre-auth keys.
    • Advertise subnet routes -- Turn one node into a gateway for an on-prem LAN by running sudo tailscale up --login-server=... --advertise-routes=192.168.1.0/24. Approve from the server with sudo headscale routes enable -r <route-id>. Instant site-to-site VPN.
    • Set up an exit node -- Mark one well-connected VPS as an exit node. Clients with tailscale up --exit-node=<ip> route all internet traffic through it. Useful for geo-fencing or consolidating egress IPs.
    • Monitor with Prometheus and Grafana -- Headscale exposes Prometheus metrics on 127.0.0.1:9090/metrics. Scrape them with a Prometheus instance and build a dashboard tracking connected nodes, DERP fallback rate, and ACL recomputation time.
    • Automate backups -- Script a nightly sqlite3 /var/lib/headscale/db.sqlite .dump | gzip > /backup/headscale-$(date +%F).sql.gz followed by an upload to S3-compatible storage. Two-minute job, saves a day of debugging during DR.
    • Integrate with your CI -- Use ephemeral pre-auth keys in GitHub Actions or GitLab CI so that build runners join the tailnet at job start, reach internal services through the mesh, and are automatically removed when the job exits.

    Deploy Headscale on a CloudCore Starter VPS
    >
    Running your own Tailscale coordination server takes less than an hour on the right infrastructure. Our CloudCore Starter plan gives you exactly what Headscale needs: 2 vCPU, 4 GB RAM, 50 GB NVMe, unmetered bandwidth — all on EU-based infrastructure for EUR 7.99/month.
    >
    - Full root SSH access from minute one
    - Free snapshot backups for the Headscale database
    - IPv4 + IPv6 included
    - Scale up to CloudCore Professional (EUR 19.99/month) or CloudCore Business (EUR 29.99+/month) as your tailnet grows
    >
    Launch Your VPS Now and start mesh-networking in under 10 minutes.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket