SSD Nodes Learn 8GB RAM — $66/yr
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-02

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 it, then join your first node.

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

Wetin 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, and dem dey build 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 a 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 you get three or four machines, plain WireGuard VPN for VPS wey you own na less software to run and less things to break. Headscale dey useful when you no longer want write one [Peer] block by hand for every new laptop. 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 sudo access. If the server dey new, first follow di first ten minutes for a new VPS.
  • One DNS A record wey dey point to dat address. Dis guide dey use headscale.example.com.
  • One second domain or subdomain for MagicDNS. Dis guide dey use tailnet.example.net. E no fit be di same domain wey dey for server_url.
  • One client machine to join, wey dey run Linux, macOS, Windows, Android or iOS.

Install headscale from the official .deb

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

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

That one go print amd64 for normal x86 VPS and arm64 for Ampere or Graviton-style plan. Put the answer for 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 ./ wey dey before the file name na required. If you no put am, apt go look for package wey dem call headscale.deb inside your repositories, then e go fail.

The package dey 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 configuration wey the package ship dey point server_url to http://127.0.0.1:8080. No client wey you get fit reach that address, so if service start now, e go dey wrong even if e come up. If you run sudo systemctl is-active headscale for this stage, e go print inactive. Na expected result be that, 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 do the same three changes. Keep the original copy, because the file long and e get plenty comments, and na the best reference wey you get for the other 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 box dey terminate TLS (transport layer security) and forward traffic go there, so nothing outside the server need reach port 8080.

base_domain na the MagicDNS suffix, the domain wey your nodes go 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 dey enough for tailnet of this size.

Headscale start na 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 go print active, and curl go print 200. enable --now dey do both parts: 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 nearly always na configuration file, because headscale dey parse the whole file before e open socket. So bad indent or unknown key go stop the process before anything listen. Fix the file, then sudo systemctl restart headscale. Every later configuration change need that same restart. Clients go reconnect by themselves afterwards. If systemd units new to you, how to run your own services and timers with systemd cover the commands we use here.

Check the state files while you still dey inside 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 to its clients. Keep am. If you delete am, headscale go generate new one, and every node go need register again.

Put TLS in front of headscale

Clients must 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 the 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 dey formatted na only cosmetic. From your laptop, curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health suppose also print 200. This one check prove say DNS, the firewall, the certificate and the proxy dey work together.

Na this proxy detail dey make people spend one evening. The Tailscale control connection na HTTP upgrade. E start with POST instead of GET, and the value of the Upgrade header na tailscale-control-protocol. Caddy pass am through without extra configuration. nginx no dey do am, so nginx front end need the 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, normal requests still succeed. Na why /health dey return 200 and everything look fine, but the long-lived control connection no dey form. Your nodes go register, then dem go remain offline. If you choose nginx, Certbot for Ubuntu 24.04 with nginx explain the certificate part.

Which ports to open in UFW

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

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

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

Create 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. The socket get mode 0770, and na the headscale group own am. Two things follow from this. The command go fail when the service stop. Na another reason why the order for this guide matter. You need sudo unless you add your own account to the headscale group.

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

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

The key dey print only once. Copy am now. Preauth key na single-use, and e dey valid for one hour unless you specify otherwise. So, e good make you set --expiration 24h while you still dey test. Add --reusable for a key wey go enroll several machines. Treat that key like password because anybody wey get 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. For the server, sudo headscale nodes list go show the node with its ID, user and online state.

The value of --login-server must match server_url exactly, including the scheme and without any trailing slash. Dem compare them as strings. If dem no match, the client go register against one address, then dem go tell am to talk to another one.

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

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

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

That form dey better for your own laptop. Preauth keys dey better for anything wey you script, because nobody need dey watch am.

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, mostly because both dey behind strict NAT (network address translation), dem go send packets through relay instead. Relay no dey hold any keys, so e no fit read your traffic. But e fit see which nodes dey communicate and how much data dey move.

Make you understand wetin default configuration dey do. Headscale ships with pointing to https://controlplane.tailscale.com/derpmap/default, together with auto_update_enabled: true and update_frequency: 3h, so na you get control plane while Tailscale own your relays. For most people, na fair trade-off. 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 talk 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 that without working embedded relay, any pair of nodes wey no fit connect directly no go connect at all.

From client, 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 remain for relay get NAT problem, no be headscale problem.

Node go show as offline because wetin?

The proxy dey drop the upgrade. Na the common one be this, and the sign be say everything else dey okay: /health dey return 200, headscale nodes list dey show the node, but the node no dey 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 to rule out the proxy.

server_url change after the nodes register. Nodes dey 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 dey log the retries for there.

The key don expire. The next section cover am.

To monitor the server side while you 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 everywhere quiet, 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 quick by design. The default na one hour and one use. If tailscale up no accept the key, generate a 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: a 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 a headless server wey nobody re-authenticate go remove itself from the network.

Do am manually 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 join be the whole server. Stop the service before you copy dem, because SQLite fit dey write data and database wey you copy while e dey under load 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 box. Dem get the private keys and every registration, so treat dem with the same care wey you give the server itself. restic backups from VPS explain how to do this on schedule and encrypted.

Upgrades dey repeat the install: 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 that version release notes first, because that same release change ACL policy behaviour and move several configuration keys.

FAQ

Why headscale no dey 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, not 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. At 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 point am to your server with sudo tailscale up --login-server https://headscale.example.com. That flag dey exist for 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 dey give out keys and addresses, while the data path na WireGuard 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 shipped configuration, those relays na Tailscale public ones. Run tailscale status on a node to see whether a given peer dey direct or dey use a relay.

Why my node dey remain offline after e register?

A node wey dey appear for headscale nodes list but never come online usually don lose the control connection for the reverse proxy. That connection na HTTP upgrade sent as a 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 dey provide quick way to test whether the proxy na the problem.

I need domain name and TLS for headscale?

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