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

How to Run Your Own Tailscale with Headscale

Run your own Tailscale control server on a VPS: install Headscale from the official .deb, set server_url, restart am, 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 na VPS wey you own dey coordinate your private network. Na community project, and Tailscale Inc. no dey run am. Every machine still dey run the official tailscale client, wey you point to your server with one flag, --login-server.

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. If you run that coordination role by yourself, na who dey hold the role change, no be wetin e fit do. So e good make you understand wetin control server fit and no fit reach for this model before you treat the move as security improvement by itself.

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 na less software to run and less thing wey fit break. Headscale become useful when you no wan write one [Peer] block by hand for every new laptop. Cost often dey make people start to look, so e make sense to read wetin the hosted free plan really cover before you take on server work, because small number of personal machines usually fit inside am. If you don already pass that limit, calculate am against wetin the paid plans cost per user and per tagged resource, because user devices no get limit, while tagged resources get separate allowances and charges. If you want self-hosted control plane but prefer your own client and web interface for managing peers instead of drop-in replacement for Tailscale, NetBird for one VPS na the alternative worth considering. Both carry your traffic over WireGuard, so the choice depend on how you write access rules and how relays behave. E worth compare NetBird with Tailscale for these points before you install either one. For broader comparison of the two models, see how WireGuard and Tailscale differ.

Wetin you need before you install

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

Install headscale from official .deb

The project dey publish .deb packages for GitHub releases page. As of October 27, 2026, the latest stable release na 0.29.6, wey dem release October 24, 2026. Check your architecture first, because na e dey show for the file name.

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.6"
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 for your repositories and e go fail.

The package dey create headscale system user, write default /etc/headscale/config.yaml, install systemd unit, then start the service. So headscale don dey run when apt return, and sudo systemctl is-active headscale go print active. But for fresh install, the package no enable the unit, so service no go come back after reboot until you enable am. The restart step below go do that. E dey also run with the shipped configuration, and that configuration point server_url to http://127.0.0.1:8080, wey no be address any of your clients fit reach. Nothing don expose yet, because the shipped listen_addr still dey on loopback, but no join any client until you set server_url and restart the service. The next two steps go do that.

Configure server_url before any client joins

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 public address wey headscale dey use when e dey build registration URLs. Clients dey use the control address wey you configure with --login-server, so use the same public name with https:// for both places, never 127.0.0.1. Headscale documentation talk say plain HTTP, or HTTPS for port wey no be 443, fit work, but e strongly recommend HTTPS for port 443 for production, and the rest of this guide assume say na so.

listen_addr na where the process dey bind. Leave am for loopback. Reverse proxy wey dey the same box go 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 go get names under. E must be fully qualified domain name without trailing dot, and e must different from the domain wey dey server_url, because the two name spaces go collide otherwise.

Leave the database section as e dey. 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.

Restart headscale and show say e dey run

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

is-active dey print active, is-enabled dey print enabled, and curl dey print 200. Restart na the step wey load the edits wey you make. Headscale dey read its configuration once, when e start, so the process wey package start go continue to use the shipped server_url until you restart am. sudo systemctl enable --now headscale no dey do this work: the service don already dey run, so the --now start no change anything and the old value remain active. enable na the job wey start service during boot, and e no happen automatically: for fresh install, package start the service without enabling am, so run sudo systemctl enable headscale once, as above.

If is-active print anything wey no be active, or curl no print 200, read the journal with sudo journalctl -u headscale -n 50 --no-pager. Failure for this stage nearly always come from the configuration file, because headscale dey parse the whole file before e open a socket. So bad indent or invalid required setting fit 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 still new to you, run your own services and timers with systemd explain the commands wey 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 package create. noise_private.key na the server identity to its clients. Keep am. If you delete am, headscale go generate new one and clients must fetch the new server key before dem fit reconnect; existing registrations remain for the database.

Put TLS for front headscale

Clients suppose 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 wey dey for 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 correctly. 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 also print 200. This one check prove say DNS, firewall, certificate, and proxy dey work together.

Na this proxy detail dey make people spend whole evening. The Tailscale control connection na HTTP upgrade. E start with POST, no be GET, and the value of the Upgrade header na tailscale-control-protocol. Caddy pass am through without extra configuration. nginx no dey do this, so nginx front end must forward the Upgrade and Connection headers explicitly. The headscale documentation do am with 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 through. Na why /health go return 200 and everything go look correct, while the control connection never form. New nodes no fit register, and existing nodes no fit reconnect and report say dem dey online. If you choose nginx, Certbot for Ubuntu 24.04 with nginx cover the certificate side.

Ports wey you suppose 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 control connection go headscale. If you later enable the embedded DERP relay, e go also need 3478/udp, as we explain below. Port 80 dey for ACME (automatic certificate management environment) HTTP challenge and redirect go HTTPS. Caddy still fit collect certificate even when port 80 close, because e fit use TLS-ALPN challenge for port 443. But if port 80 open, both challenges dey available and plain http:// requests go redirect.

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 for UFW no go expose anything, because nothing dey listen on am from outside, and e no add any benefit. Remember say most providers get another firewall for their control panel, separate from UFW. So port fit open for the server and still close for the edge. UFW firewall basics for VPS explain the rule syntax in 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 headscale group own am. Two things follow from this. The command go fail when 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 go 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 go print only once. Copy am now. Preauth key na single-use and e valid for one hour unless you specify another setting. So, --expiration 24h dey worth setting while you still dey test. Add --reusable for 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 dey 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 its ID, user, and online state.

Use the public HTTPS address from server_url for --login-server. The client dey remove trailing slashes from its control URL; no exact string comparison dey require make you remove one.

Machine wey previously sign in to Tailscale hosted service 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 one URL instead. Open am, and 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 better for your own laptop. Preauth keys better for anything wey you script, because nobody need dey watch am. Once the VPS don become node, e fit also carry internet traffic for your other machines. Na the exit node setup be this. The difference be say you approve the advertised route for the server with the headscale command, instead of approving am for hosted admin console. If wetin you want na access to private network wey dey behind that VPS, instead of route go internet, the same approval step cover advertising that subnet to the rest of your tailnet. Publishing one application from a node, instead of routing whole networks through am, na another job. serve and funnel are the two ways to do it. But both depend on Tailscale own certificate and ingress machinery, so treat dem as hosted-tailnet features, not something wey headscale provide.

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 go send packets through relay instead. Relay no hold any keys, so e no fit read your traffic. But e fit see which nodes dey talk 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 already set, so na you own control plane, but Tailscale own your relays. For most people, na fair trade-off. If e no work for you, run your own relay.

To run your own relay, set enabled: true under derp.server inside config.yaml. Replace the shipped ipv4: 198.51.100.1 and ipv6: 2001:db8::1 with your relay public addresses, or remove both settings to use DNS. Then restart headscale and open the STUN (session traversal utilities for NAT) port with sudo ufw allow 3478/udp. Those address settings override DNS, so if you leave any placeholder, clients go try connect to address wey no reachable. The configuration file make the requirement clear: server_url must use https, because DERP needs TLS. If you empty derp.urls list, e remove Tailscale relays from the map. Make sure say you get working embedded or custom relay before you do that. Headscale no go start with empty relay map. If map get only one unreachable relay, nodes wey no fit connect directly no go fit communicate.

From client, tailscale netcheck go print latency to each relay region wey e know. tailscale status go show direct with an address or relay with region code for active peer connections. If peer remain stuck on relay, check the direct path, including NAT and firewall rules. If peer dey direct and e still slow, na another issue. The usual answer for that one na MTU, not the tunnel itself.

Node dey show as offline because of wetin?

Proxy dey drop the upgrade. Sign wey show this na when /health return 200 but new registrations dey fail or existing nodes no fit reconnect. The control connection na POST wey carry Upgrade: tailscale-control-protocol, and proxy wey no forward am go block both registration and the channel wey dey report node state. Compare your nginx configuration with the upgrade lines wey dey above, or switch to Caddy to confirm whether proxy na the problem.

Public control address don change. Nodes dey continue dial the address wey dem configure with --login-server; changing server_url no go update that client setting. If old address no longer reach your server, run sudo tailscale up --login-server https://headscale.example.com --force-reauth for each node.

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.

Key don expire. The next section cover this one.

To monitor the server side while you dey test, run sudo journalctl -u headscale -f for the VPS and restart tailscaled for the client. Check the client journal too: request wey reach headscale no guarantee say e go produce line for the default log level, so server log silence alone no prove DNS, firewall or proxy failure.

Key expiry, and the node wey stop working weeks later

Two different expiry dey, and if you mix dem up, e 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 new one for the server instead of editing anything for the client.

Node keys na the long-lived part. The node section of config.yaml sets expiry: 0, and 0 means say no default expiry dey; explicit client expiry or OIDC expiry fit still apply. Tagged nodes no dey affected by automatic expiry, though administrator fit expire dem explicitly. Set expiry: 180d if you want 180-day default for registrations wey no get tag, but expiry wey client request take priority, and OIDC fit override the default. Changing this setting no go change existing registrations retroactively. Node wey get expiry must authenticate again before that deadline, using sudo tailscale up --login-server https://headscale.example.com --force-reauth. Headless server wey nobody authenticate again go comot from the network when its registration expire.

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 na the whole server together. Stop the service before you copy dem, because SQLite fit still dey write, and database wey you copy while load dey happen fit get inconsistent data.

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 machine. Dem contain the private keys and all the registrations, so treat dem with the same care wey you go give the server itself. restic backups from VPS explain how to do this on schedule and with encryption.

Upgrades dey repeat the installation 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. You no fit skip a minor version, and you no fit downgrade to an older minor version. Move one minor version at a time, take backup before every step, and read the release notes for that version first, because that release change ACL policy behaviour and move some configuration keys. Version 0.29.6 also get security updates for certificate validation and dependencies, so existing 0.29 installations suppose install this patch release.

FAQ

Headscale dey already run immediately after I install the .deb?

Yes. The package start the service as soon as e install, with the default /etc/headscale/config.yaml, wey dey listen for loopback only and set server_url to http://127.0.0.1:8080. For fresh install, e no enable the service, so e no go start during boot until you run sudo systemctl enable headscale. The default address no useful to clients, so edit server_url, listen_addr and base_domain, then run sudo systemctl restart headscale and confirm with sudo systemctl is-active headscale. E must be restart: headscale dey read the file only when e start, and enable --now for service wey already dey run go enable am for boot but e no restart am, so the old values go remain active. If e fail after the restart, sudo journalctl -u headscale -n 50 --no-pager go show the problem, and 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 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 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 the data path na WireGuard directly between your nodes. Traffic only take detour when two nodes no fit reach each other directly and dem fall back to a DERP relay, and with the default configuration those relays na Tailscale public ones. Run tailscale status for a node to see whether a particular peer dey direct or dey use a relay.

Why my node remain offline after e register?

If node show for headscale nodes list but e never come online, check the control connection, including the reverse proxy. That connection na an HTTP upgrade wey dem send as a POST with the header Upgrade: tailscale-control-protocol, and nginx go drop am unless you forward the Upgrade and Connection headers explicitly with proxy_set_header, wey the configuration above dey do through its map $http_upgrade $connection_upgrade block. If dem drop those headers, new nodes no go register too. Caddy dey forward am without extra configuration, so e fit help test quickly whether na the proxy cause the problem.

I need domain name and TLS for headscale?

For this setup, yes: use domain name and HTTPS. Certificates fit also cover bare IP addresses, but the configuration file state say DERP require TLS. Domain plus Caddy dey take about five minutes and give you HTTPS endpoint wey dey renew itself. Plain HTTP dey possible, but headscale documentation strongly recommend HTTPS for port 443 in production, and note say Tailscale client dey assume HTTPS for port 443 in some situations.

Dis article help you?