SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-28

How to Self-Host Tailscale with Headscale

Run your own Tailscale control server on VPS. Install headscale from the official .deb, set server_url before starting, then join your first node.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Wetín headscale be

Headscale na self-hosted implementation of the Tailscale control server. So, the machine wey dey coordinate your private network na VPS wey you own. Na community project, and Tailscale Inc. no dey run am. Every machine still dey run the official tailscale client, pointed to your server with one flag, --login-server.

The control server na the part wey know who belong to the network. E dey give each node address from 100.64.0.0/10, distribute public keys, and tell nodes where dem fit find each other. The tunnels still be WireGuard, built node to node. Traffic between two of your machines no dey pass through the headscale box, unless direct path no fit build and the nodes fall back to relay.

Headscale dey serve one tailnet (one Tailscale network) for each instance. The project describe am as suitable for personal use or small organisation. If na three or four machines, plain WireGuard VPN for VPS wey you own mean less software to run and less things to break. Headscale dey useful when you no wan write one [Peer] block by hand for every new laptop. If you want self-hosted control plane but you prefer your own client and web interface to manage peers instead of drop-in replacement for Tailscale, NetBird for one VPS na the alternative wey worth consider. For wider comparison of the two models, see how WireGuard and Tailscale dey differ.

Wetin you need before you install

  • A VPS wey dey run Ubuntu 24.04, get public IPv4 address, and you get sudo access. If na new server, first follow the first ten minutes on a new VPS.
  • A DNS A record wey dey point to that address. This guide dey use headscale.example.com.
  • Another domain or subdomain for MagicDNS. This guide dey use tailnet.example.net. E no fit be the same domain wey dey for server_url.
  • One client machine wey you wan join, and e dey run Linux, macOS, Windows, Android or iOS.

Headscale install from official .deb

Project dey publish .deb packages for GitHub releases page. As of July 2026, current release na 0.29.3. Check your architecture first, because the file name get the architecture inside am.

sudo apt update
sudo apt install -y wget
dpkg --print-architecture

That command go print amd64 for normal x86 VPS and arm64 for Ampere or Graviton-style plan. Put the answer inside the variable below.

HEADSCALE_VERSION="0.29.3"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb \\
  "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install -y ./headscale.deb
headscale version

The ./ before the file name na required. If you leave am out, apt go look for package wey dem call headscale.deb inside your repositories, then e go fail.

The package go create headscale system user, write default /etc/headscale/config.yaml, and install systemd unit. E no start the service, and na the correct order be that. The shipped configuration point server_url to http://127.0.0.1:8080. No client of yours fit reach that address, so service wey start now go dey wrong even if e come up. If you run sudo systemctl is-active headscale for this stage, e go print inactive. That one na expected behaviour, no be fault.

Configure server_url before you start the service

Edit /etc/headscale/config.yaml with sudo nano /etc/headscale/config.yaml, or use sed apply the same three changes. Keep copy of the original, because the file long and e get plenty comments, and na the best reference wey you get for the remaining settings.

sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.orig
sudo sed -i 's|^server_url:.*|server_url: https://headscale.example.com|' /etc/headscale/config.yaml
sudo sed -i 's|^listen_addr:.*|listen_addr: 127.0.0.1:8080|' /etc/headscale/config.yaml
sudo sed -i 's|^  base_domain:.*|  base_domain: tailnet.example.net|' /etc/headscale/config.yaml
sudo grep -E '^(server_url|listen_addr):|^  base_domain:' /etc/headscale/config.yaml

server_url na the address wey headscale dey write inside every client registration. Clients go dial that exact string forever after, so e must be the public name with https:// for front, never 127.0.0.1.

listen_addr na where the process dey bind. Leave am for loopback. A reverse proxy for the same server dey terminate TLS (transport layer security) and forward traffic go am, so nothing outside the server need reach port 8080.

base_domain na the MagicDNS suffix, the domain wey your nodes dey get names under. E must be a fully qualified domain name without trailing dot, and e must different from the domain for server_url, because the two name spaces go collide otherwise.

Leave the database section as e be. The default na SQLite for /var/lib/headscale/db.sqlite, inside directory wey the package create and own, and SQLite enough for tailnet of this size.

Start headscale and prove say e dey run

sudo systemctl enable --now headscale
sudo systemctl is-active headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/health

is-active dey print active and curl dey print 200. enable --now dey handle both parts of the work: e start the service and mark am to start after reboot.

If is-active print failed, read the journal with sudo journalctl -u headscale -n 50 --no-pager. Failure for this stage almost always come from the configuration file, because headscale parse the whole file before e open socket. So bad indentation or unknown key fit stop the process before anything start listening. Fix the file, then run sudo systemctl restart headscale. Every configuration change afterwards need that same restart. Clients go reconnect by themselves. If systemd units still new to you, run your own services and timers with systemd explain the commands we use here.

Check the state files while you still dey for the shell:

stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.key

Both lines start with headscale, the unprivileged user wey the package create. noise_private.key na the server identity wey e use with clients. Keep am. If you delete am, headscale go generate new one and every node go need register again.

Put TLS for front of headscale

Clients gatz reach server_url through HTTPS. Caddy na the shortest way, because e dey request and renew the certificate by itself.

sudo apt install -y caddy

Replace /etc/caddy/Caddyfile with the block from headscale documentation:

headscale.example.com {
    reverse_proxy 127.0.0.1:8080 {
        header_up True-Client-IP {remote_host}
        header_up X-Real-IP {remote_host}
    }
}
sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl is-active caddy

validate go print adapted config to JSON when the file parse well. Warning say the file no formatted na only cosmetic matter. From your laptop, curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health suppose still print 200. This one check prove say DNS, firewall, certificate and proxy all dey work together.

Here na the proxy detail wey fit make person spend whole evening. Tailscale control connection na HTTP upgrade. E dey start with POST instead of GET. The value of the Upgrade header na tailscale-control-protocol. Caddy dey pass am through without extra configuration. nginx no dey do this by default, so nginx front end need this upgrade map:

map $http_upgrade $connection_upgrade {
    default keep-alive;
    ''      close;
}

server {
    listen 443 ssl;
    server_name headscale.example.com;
    location / {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        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_buffering off;
        proxy_pass http://127.0.0.1:8080;
    }
}

If you leave those lines out, ordinary requests still go succeed. Na why /health go return 200 and everything go look correct, but the long-lived control connection no go form. Your nodes go register, then dem go remain offline. If you choose nginx, Certbot for Ubuntu 24.04 with nginx cover the certificate part.

Ports wey you need open for UFW

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Port 443 dey carry every client conversation. Port 80 dey there only for ACME (automatic certificate management environment) HTTP challenge and redirect go HTTPS, and Caddy need am to get certificate at all.

Port 8080 remain closed. listen_addr na 127.0.0.1:8080, so the proxy dey reach headscale through loopback interface and no firewall rule dey involved. Opening 8080 to internet go give clients cleartext control channel and no benefit. Remember say most providers get second firewall for their control panel, separate from UFW. So port fit dey open for the box and still dey closed for the edge. UFW firewall basics for VPS explain the rule syntax with more detail.

Make user and preauth key

sudo headscale users create alice
sudo headscale users list

The headscale command na client. E dey talk to the daemon wey dey run through the Unix socket for /var/run/headscale/headscale.sock. Socket mode na 0770, and headscale group own am. Two things follow from this. Command go fail if service stop, and na another reason why the order for this guide matter. You need sudo unless you add your own account to headscale group.

users list dey print ID beside each name. You need that number because key command dey take numeric user ID, e no dey take name.

sudo headscale preauthkeys create --user 1 --expiration 24h

The key go print only once. Copy am now. Preauth key na single-use, and e dey valid for one hour unless you specify otherwise. So --expiration 24h dey worth setting while you still dey test. Add --reusable for key wey fit enroll several machines. Treat that key like password because anybody wey hold am fit join your network.

Connect your first client with --login-server

For the machine wey you wan join:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com --auth-key 'hskey-auth-PASTE-YOUR-KEY-HERE'
tailscale status
tailscale ip -4

tailscale ip -4 go print the address wey headscale assign, something like 100.64.0.1. Go back to the server; sudo headscale nodes list go show the node with e ID, e user, and whether e dey online.

The value for --login-server must match server_url exactly, including the scheme, and e no suppose get trailing slash. Dem dey compare dem as strings, so if dem no match, the client go register against one address, then dem go tell am to talk to another one.

If machine don previously sign in to Tailscale hosted service, e go keep that login. Run sudo tailscale logout for am first, then run tailscale up with --login-server.

If you leave out --auth-key, the client go print URL instead. Open the URL, and the page go show the identifier for that registration attempt. You go approve am for the server:

sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGE

That form better for your own laptop. Preauth keys better for anything wey script dey run, because nobody need dey watch am. Once the VPS itself don become node, e fit also carry internet traffic for your other machines. Na the exit node setup be that. The difference be say you approve the advertised route for the server with the headscale command, instead of approving am inside hosted admin console.

DERP, and wetin dey relay traffic when direct path fail

DERP (designated encrypted relay for packets) na fallback path. When two nodes no fit open direct WireGuard connection, usually because both dey behind strict NAT (network address translation), dem send packets through relay instead. Relay no hold any key, so e no fit read your traffic. E fit see which nodes dey communicate and how much data dey move.

Make you understand wetin default configuration dey do. Headscale ships with https://controlplane.tailscale.com/derpmap/default, auto_update_enabled: true, and update_frequency: 3h, so na you own control plane, but na Tailscale own relays. For most people, this trade-off fair. If e no suit you, run your own.

To run your own relay, set enabled: true under derp.server for config.yaml, restart headscale, and open the STUN (session traversal utilities for NAT) port with sudo ufw allow 3478/udp. The configuration file state the requirement clearly: server_url must use https, because DERP need TLS. If you empty derp.urls list, e go remove Tailscale relays from the map. If you do this without working embedded relay, any pair of nodes wey no fit connect directly no go connect at all.

From client side, tailscale netcheck go print latency to each relay region wey e know, and tailscale status go mark every peer as either direct with address or relay with region code. Peer wey dey stuck for relay na NAT problem, no be headscale problem. Peer wey be direct but still slow na another matter again, and the usual answer for there na MTU, no be the tunnel itself.

Node why e dey show as offline?

Proxy dey drop the upgrade. Na this one common pass, and the sign be say everything else dey healthy: /health returns 200, headscale nodes list dey show the node, but the node never come online. The control connection na POST wey carry Upgrade: tailscale-control-protocol, and proxy wey no forward am go kill the only channel wey dey report node state. Compare your nginx configuration with the map block wey dey above, or switch to Caddy make you rule out proxy problem.

server_url change after the nodes register. Nodes continue to dial the value wey dem receive during registration. If you edit am, run sudo tailscale up --login-server https://headscale.example.com --force-reauth for each node.

The client no dey run. For the node, sudo systemctl is-active tailscaled and sudo journalctl -u tailscaled -n 50 --no-pager. Client wey no fit resolve or reach your domain go log its retries there.

The key don expire. The next section cover am.

To monitor the server side as you dey test, run sudo journalctl -u headscale -f for the VPS and restart tailscaled for the client. Node wey reach headscale go produce log lines immediately. If no log appear, e mean say the request no dey arrive, so check DNS, the firewall and the proxy before you check headscale.

Key expiry, and the node wey stop to work weeks later

Two different expiry dey, and if you mix dem up, you go waste time.

Preauth keys dey expire quickly by design. Default na one hour and one use. If tailscale up no accept the key, generate fresh one for the server instead of editing anything for the client.

Node keys na the long-lived part. The node section for config.yaml dey set expiry: 0, and 0 mean say no default expiry dey: registered node go remain valid until you expire am. Tagged nodes no dey ever expire, regardless. Set expiry: 180d if you want registrations to expire after some time, and understand wetin you dey request: every non-tagged node go then need sudo tailscale up --login-server https://headscale.example.com --force-reauth on that schedule, and headless server wey nobody re-authenticate go remove itself from the network.

Do am by hand when person lose laptop. sudo headscale nodes list go give you the ID, then sudo headscale nodes expire -i 3 go log that node out, and sudo headscale nodes delete -i 3 go remove am from the network completely.

Backups and upgrades

/var/lib/headscale and /etc/headscale together na the whole server. Stop the service before you copy dem, because SQLite fit still dey write, and database wey you copy while load dey happen fit no consistent.

sudo systemctl stop headscale
sudo tar czf /root/headscale-state.tgz -C /var/lib headscale
sudo tar czf /root/headscale-config.tgz -C /etc headscale
sudo systemctl start headscale
sudo chmod 600 /root/headscale-*.tgz

Move both files comot from the server. Dem contain the private keys and every registration, so handle dem with the same care wey you give the server itself. restic backups from VPS explain how to do this on schedule and with encryption.

Upgrades dey repeat the install process: download the new .deb, sudo apt install ./headscale.deb, then restart and run the is-active and /health checks again. Since 0.29, the upgrade path strict. E no allow you skip minor version, and e no allow downgrade to older minor version. Move one minor version at a time, take backup before each step, and read the release notes for that version first, because that same release change ACL policy behaviour and move several configuration keys.

FAQ

Why headscale dey fail to start immediately after I install the .deb?

The package dey install the unit but e leave the service stopped, and the default /etc/headscale/config.yaml na template instead of working configuration. Edit server_url, listen_addr and base_domain first, then run sudo systemctl enable --now headscale and confirm with sudo systemctl is-active headscale. If e still fail, sudo journalctl -u headscale -n 50 --no-pager go show the problem. For this stage, na almost always YAML error, because headscale dey parse the whole file before e bind a port.

I still need install the normal Tailscale client for my machines?

Yes. Headscale dey replace only the control server. Every node dey run the official client from Tailscale, and you go point am to your server with sudo tailscale up --login-server https://headscale.example.com. That flag dey inside the standard client, so nothing need patching or rebuilding.

My traffic dey pass through the headscale server?

Usually, no. Headscale dey coordinate the network and distribute keys and addresses, while WireGuard dey carry the data directly between your nodes. Traffic only dey take another route when two nodes no fit reach each other directly and dem fall back to a DERP relay. With the configuration wey come with the package, those relays na Tailscale public ones. Run tailscale status for one node to see whether a given peer dey direct or dey use a relay.

Why my node dey remain offline after e register?

If node dey show for headscale nodes list but e never come online, e usually mean say e lose the control connection for the reverse proxy. That connection na HTTP upgrade wey dey send as POST with the header Upgrade: tailscale-control-protocol. nginx go drop am unless you add the map $http_upgrade $connection_upgrade block and the matching proxy_set_header lines. Caddy dey forward am without extra configuration, so e fit help you quickly test whether na the proxy cause the problem.

I need domain name and TLS for headscale?

For practical use, yes. Clients dey connect to the string wey you put for server_url. Certificates dey issue for names, not bare IP addresses, and the configuration file dey state say DERP require TLS. Domain plus Caddy fit take about five minutes, and e go give you HTTPS endpoint wey dey renew by itself. If you run the control server over plain HTTP, every client conversation with am go cross the internet without encryption.