All tutorials
14 min read

How to Self-Host Headscale on Ubuntu 24.04 (Tailscale Control Server)

Self-host Headscale 0.29 on Ubuntu 24.04 with Let's Encrypt HTTPS, join devices through the Tailscale apps, and set up MagicDNS, an exit node and backups.

TL;DR:

  • Install Headscale 0.29.4 from its official .deb package, serve it on port 443 with the built-in Let's Encrypt client, and join devices with the Tailscale apps, version 1.80.0 or newer.
  • Open 443/tcp for clients and 80/tcp for the Let's Encrypt check; 3478/udp only for the embedded DERP relay.
  • Windows and Linux join with a pre-auth key and tailscale up --login-server. macOS, iPhone and Android join through the app's custom server setting and headscale auth register.
  • MagicDNS names devices under dns.base_domain, which must differ from the domain in server_url.
  • Headscale's FAQ calls the Tailscale client on the Headscale server unsupported, so the exit node runs on a second server, approved with headscale nodes approve-routes.
  • Back up /etc/headscale and /var/lib/headscale before every upgrade, and upgrade one minor version at a time.

Applies to: Ubuntu 24.04 LTS · Headscale 0.29.4 · Tailscale clients 1.80.0 and newer · checked October 2026

Headscale is an open-source (BSD-3-Clause) implementation of the Tailscale control server that registers devices, exchanges their WireGuard public keys, assigns tailnet IP addresses and pushes DNS and route settings to the official Tailscale clients. This tutorial installs Headscale 0.29.4 on Ubuntu 24.04 behind HTTPS on your own domain, joins Windows, macOS, iPhone and Android devices, and covers MagicDNS, an exit node, backups and upgrades. A VPS gives the control server a fixed public IP address that every device can reach.

What Headscale replaces and what it does not

Tailscale's hosted service runs the control server for you. Headscale replaces that one part:

PartWith Headscale
Control server: registration, keys, IP addresses, users, routes, DNSHeadscale on your server, one tailnet per server
Client appsThe official Tailscale apps
Relays (DERP) for devices that cannot connect directlyTailscale's public DERP servers by default, or Headscale's optional embedded relay
Admin interfaceThe headscale command line. Web interfaces are community projects
Not in Headscale 0.29.4Funnel, Serve and network flow logs

Device traffic runs over WireGuard, directly between devices or through a DERP relay. Headscale carries none of it unless you turn on its embedded relay, and by default it tells clients to turn off log submission to Tailscale Inc. For one WireGuard server with hand-written peer configs instead, see How to Set Up a WireGuard VPN Server on Ubuntu 24.04.

Prerequisites

  • A VPS running Ubuntu 24.04 LTS or 26.04 LTS with a public IPv4 address. Headscale sets no CPU or RAM minimum. Ubuntu's minimum for cloud images is 1 GB of RAM and 4 GB of storage, which the Arct Cloud Cost Optimized cvm.pico plan meets (1 vCPU, 1 GB RAM, 15 GB NVMe, 1 TB of outbound transfer per month); only outbound transfer counts toward the allowance, and personal VPN use is allowed for lawful purposes only.
  • A non-root user with sudo privileges and SSH key login. How to Generate an SSH Key on Windows, macOS, and Linux creates both.
  • A domain where you can create DNS records, and nothing else listening on ports 80 or 443. To keep an existing reverse proxy, read the reverse proxy section before Step 4.
  • The Tailscale app, version 1.80.0 or newer, on each device, from Tailscale's download page.
  • For the optional exit node: a second Ubuntu 24.04 server in the place whose IP address websites should see.

Commands run as the sudo user on the Headscale server unless a step says otherwise. Replace hs.example.com (Headscale), tail.example.com (MagicDNS names) and values in capitals, such as YOUR_AUTH_KEY, with your own.

Step 1: Point a DNS record at the server

Tailscale clients and Let's Encrypt both reach Headscale by its hostname. Create a DNS A record for hs.example.com with the server's IPv4 address, and an AAAA record if your server has a public IPv6 address. On Cloudflare, set the record to DNS only: Headscale's documentation says Cloudflare's proxy does not support the WebSocket POST requests the Tailscale protocol uses. Check the record:

getent ahostsv4 hs.example.com

The output looks similar to this:

203.0.113.10    STREAM hs.example.com
203.0.113.10    DGRAM
203.0.113.10    RAW

If you added an AAAA record, getent hosts hs.example.com shows the IPv6 address.

Step 2: Open ports 80 and 443 in UFW

Port 443 serves the Tailscale clients, and port 80 answers the Let's Encrypt HTTP-01 check. Allow SSH first so the firewall keeps your session:

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

Type y to confirm. If SSH listens on another port, replace OpenSSH with YOUR_SSH_PORT/tcp. If your provider runs a network firewall in front of the server, open the same ports there. The output looks similar to this:

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)

Step 3: Install Headscale from the official package

Headscale recommends its DEB package on Ubuntu 22.04 and newer. It creates an unprivileged headscale service user, installs /etc/headscale/config.yaml and a systemd unit, and starts the service on 127.0.0.1:8080. Download version 0.29.4 and its checksum file, then verify the download:

HEADSCALE_VERSION="0.29.4"
HEADSCALE_ARCH="$(dpkg --print-architecture)"
curl -fLO "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
curl -fLO "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/checksums.txt"
sha256sum --ignore-missing -c checksums.txt

The output ends with:

headscale_0.29.4_linux_amd64.deb: OK

Install the package from the same shell and check the version:

sudo apt install "./headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
dpkg-query -W headscale
headscale	0.29.4

0.29.4 was the latest release on 2026-10-06. Read the update section before you install a newer one from the releases page.

Step 4: Set server_url, HTTPS and MagicDNS

Open the config file:

sudo nano /etc/headscale/config.yaml

Change these three top-level keys. Each appears once:

server_url: https://hs.example.com
listen_addr: 0.0.0.0:443
tls_letsencrypt_hostname: "hs.example.com"

server_url is the address every client uses. listen_addr moves Headscale to port 443 on all IPv4 and IPv6 addresses. tls_letsencrypt_hostname turns on the built-in Let's Encrypt client, which uses the HTTP-01 check on port 80 and stores the certificate in /var/lib/headscale/cache.

In the dns: section, change base_domain:

dns:
  magic_dns: true
  base_domain: tail.example.com

Devices get names such as laptop.tail.example.com. base_domain must not equal or contain the server_url domain: example.com with hs.example.com stops Headscale from starting. The nameservers list below it uses Cloudflare's 1.1.1.1 and 1.0.0.1 and their two IPv6 addresses; replace all four with resolvers you trust. Save with Ctrl+O and exit with Ctrl+X.

Step 5: Start Headscale and check HTTPS

Enable the service at boot, restart it with the new config, and read the log:

sudo systemctl enable headscale
sudo systemctl restart headscale
sudo journalctl -u headscale -n 20 --no-pager

The log includes a line similar to this:

listening and serving HTTP on: 0.0.0.0:443

Check the health endpoint over HTTPS:

curl https://hs.example.com/health
{"status":"pass"}

The first HTTPS request makes Headscale fetch the certificate from Let's Encrypt, so it answers more slowly than later ones. Headscale renews the certificate when 30 days or less remain.

Step 6: Create a user and a pre-auth key

Every personal device belongs to a Headscale user. Run the headscale command with sudo: its socket is open only to root and the headscale group. Replace alice with your own user name:

sudo headscale users create alice
sudo headscale users list

The output looks similar to this:

User created
ID | Name | Username | Email | Created
1  |      | alice    |       | 2026-10-06 09:15:41

Create a pre-auth key. --user takes the numeric ID from the list, not the name:

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

The command prints a key that starts with hskey-auth-. It works once and expires after one hour. Create one key per device, or add --reusable to use one key for several devices while it is valid.

Step 7: Connect Windows, macOS and Linux computers

Windows

Install Tailscale, then log in from Command Prompt or PowerShell with the key from Step 6:

tailscale up --login-server https://hs.example.com --auth-key YOUR_AUTH_KEY

To keep Tailscale connected after you sign out of Windows, open Preferences from the tray icon and turn on Run unattended.

macOS

Option-click the Tailscale icon in the menu bar and hover over Debug. Under Custom Login Server, select Add Account..., enter https://hs.example.com and click Add Account. With the Tailscale command line on macOS, tailscale login --login-server https://hs.example.com opens the same page. The browser opens a Node registration page with a command like headscale auth register --auth-id AUTH_ID --user USERNAME. Run it on the server with the auth ID from the page:

sudo headscale auth register --auth-id YOUR_AUTH_ID --user alice

The output looks similar to this:

Node alices-macbook-pro registered

Linux

Install Tailscale from Tailscale's package repository, then log in with a pre-auth key:

sudo tailscale up --login-server https://hs.example.com --auth-key YOUR_AUTH_KEY

Step 8: Connect an iPhone or Android phone

  • iPhone: open Tailscale, tap the account icon in the top-right corner and select Log in…. Tap the options menu in the top right, select Use custom coordination server and enter https://hs.example.com.
  • Android: open the settings menu in the upper-right corner and tap Accounts. In the three-dot menu, select Use an alternate server and enter https://hs.example.com.

The app opens the Node registration page. Approve the phone on the server with the auth ID from the page:

sudo headscale auth register --auth-id YOUR_AUTH_ID --user alice

The phone connects once the registration completes. Android also accepts a pre-auth key under Use an auth key.

Step 9: Verify devices and MagicDNS

List the registered devices on the server:

sudo headscale nodes list

Each device shows an address from 100.64.0.0/10, its MagicDNS name in the Name column and online under Connected. On a computer in the tailnet, run:

tailscale status
tailscale ping YOUR_DEVICE_NAME
tailscale dns query YOUR_DEVICE_NAME.tail.example.com

tailscale status marks each active connection direct (peer-to-peer) or relay (through DERP). tailscale dns query returns the device's 100.64.x.x address. Headscale adds tail.example.com as the first search domain, so short names work in apps and browsers too.

Use a VPS as a Tailscale exit node (optional)

An exit node sends the internet traffic of the devices that select it out through its own public IP address, like a VPN server. Headscale's FAQ says the Tailscale client on the Headscale server is not supported, so use a second Ubuntu 24.04 server. Route only your own lawful traffic through it. Every byte routed through it leaves that server once, toward your device or the website, plus WireGuard headers.

On the second server, add Tailscale's apt repository and install the client. On Ubuntu 26.04, replace noble with resolute:

sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg > /dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list
sudo apt-get update
sudo apt-get install tailscale

Turn on IP forwarding with the commands from Tailscale's documentation:

echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

If UFW is active there, sudo ufw allow 41641/udp helps devices connect peer-to-peer instead of through a relay. If your provider runs a network firewall, open the same port there. Create a new pre-auth key on the Headscale server (Step 6), then join the exit server and advertise it:

sudo tailscale up --login-server https://hs.example.com --auth-key YOUR_AUTH_KEY --hostname exit --advertise-exit-node

On the Headscale server, list the advertised routes:

sudo headscale nodes list-routes

The output looks similar to this:

ID | Hostname | Approved | Available | Serving (Primary)
3  | exit     |          | 0.0.0.0/0 |
   |          |          | ::/0      |

Approve the IPv4 route with the ID from the first column. Headscale approves ::/0 with it and prints Node updated:

sudo headscale nodes approve-routes --identifier 3 --routes 0.0.0.0/0

Select exit on each device: Use exit node in the Windows tray menu, Exit Nodes in the macOS menu bar, or the Exit Node section of the iPhone and Android apps. On Linux, run sudo tailscale set --exit-node=YOUR_EXIT_NODE_IP with the exit server's address from tailscale status. A what-is-my-IP page now shows the exit server's public IP address.

Turn on the embedded DERP relay (optional)

Devices that cannot connect directly relay through Tailscale's public DERP servers. Headscale can add its own relay. In /etc/headscale/config.yaml, set these keys in the derp: section:

derp:
  server:
    enabled: true
    ipv4: YOUR_SERVER_IP
    ipv6: YOUR_SERVER_IPV6

The example file ships documentation addresses on both lines. If your server has no public IPv6 address, delete the ipv6 line. The relay uses port 443 and STUN on UDP 3478:

sudo ufw allow 3478/udp
sudo systemctl restart headscale

If your provider runs a network firewall in front of the server, open 3478/udp there too.

tailscale debug derp headscale on a client checks the relay. Keep Tailscale's map in derp.urls, because Headscale's documentation calls a single relay a single point of failure.

Use a reverse proxy instead of built-in HTTPS

If Nginx, Caddy or Apache installed on the server already serves ports 80 and 443, let the proxy handle TLS. In Step 4, keep the https:// server_url, set listen_addr: 127.0.0.1:8080, leave tls_letsencrypt_hostname empty, and replace the existing trusted_proxies: [] line with:

trusted_proxies:
  - 127.0.0.1/32
  - ::1/128

The proxy must pass WebSocket upgrades, including the client's POST request with the Upgrade: tailscale-control-protocol header. Headscale's community-maintained reverse proxy page has Nginx, Caddy and Apache examples. This setup needs the proxy installed on the host. A proxy inside a Docker container, such as Nginx Proxy Manager, cannot reach 127.0.0.1 on the host, because that address is the container itself. The startup warning listening without TLS but ServerURL does not start with http:// is expected in this setup.

Back up Headscale

/etc/headscale holds the config, and /var/lib/headscale holds the SQLite database, private keys and certificate cache. Stop the service first, as Headscale's upgrade guide does, so the database is copied in a consistent state:

sudo systemctl stop headscale
TIMESTAMP=$(date +%Y%m%d%H%M%S)
sudo cp -aR /etc/headscale /etc/headscale.backup-$TIMESTAMP
sudo cp -aR /var/lib/headscale /var/lib/headscale.backup-$TIMESTAMP
sudo systemctl start headscale

Pack both copies into one archive your user can download:

sudo tar -czf ~/headscale-backup-$TIMESTAMP.tar.gz /etc/headscale.backup-$TIMESTAMP /var/lib/headscale.backup-$TIMESTAMP
sudo chown "$USER": ~/headscale-backup-$TIMESTAMP.tar.gz
chmod 600 ~/headscale-backup-$TIMESTAMP.tar.gz

The archive contains private keys, so store it like a password. Download it as shown in How to Copy Files to a VPS with rsync and scp. For whole-server copies, see VPS backups.

To restore, replace TIMESTAMP with the suffix of the backup you want and run:

sudo systemctl stop headscale
sudo mv /etc/headscale /etc/headscale.old
sudo mv /var/lib/headscale /var/lib/headscale.old
sudo cp -aR /etc/headscale.backup-TIMESTAMP /etc/headscale
sudo cp -aR /var/lib/headscale.backup-TIMESTAMP /var/lib/headscale
sudo systemctl start headscale

Update Headscale

Headscale blocks skipped minor versions and downgrades: go from 0.29.x to 0.30.x, then 0.31.x, with the latest patch release each time.

  1. Read the release notes for breaking changes and the minimum Tailscale client version.
  2. Back up Headscale as shown above.
  3. Run the Step 3 commands with the new HEADSCALE_VERSION. If apt asks about the modified /etc/headscale/config.yaml, keep your version, the default answer. The package restarts Headscale as part of the upgrade.
  4. Compare your config with the new example and fix any removed keys. If Headscale fails to start, sudo journalctl -u headscale -n 20 --no-pager names the key it rejects. Version 0.29.0, for example, refuses to start while the removed randomize_client_port key is set:
diff /etc/headscale/config.yaml /usr/share/doc/headscale/examples/config-example.yaml
  1. Restart Headscale and check dpkg-query -W headscale, the /health endpoint and sudo headscale nodes list.

Troubleshoot common Headscale errors

SymptomCauseFix
binding to TCP address (port 443) or failed to set up a HTTP server (port 80), with address already in use in the logAnother web server holds port 443 or 80Stop it, or use the reverse proxy setup
server_url cannot use the same domain as base_domain or server_url cannot be part of base_domainbase_domain equals or contains the server's domainSet base_domain to tail.example.com
curl to /health fails with a TLS errorDNS points elsewhere, or port 80 is blockedCheck getent ahostsv4, sudo ufw status and any provider firewall
can't change --login-server without --force-reauthThe device is signed in to another control serverAdd --force-reauth to tailscale up
Exit node selected, but websites do not loadIP forwarding is off, or the route is not approvedCheck sysctl net.ipv4.ip_forward on the exit server and the Approved column of list-routes

FAQ

What are the key differences between Headscale and Tailscale?

Tailscale runs the control server for you as a hosted service; Headscale is an open-source control server you run yourself. Both use the same Tailscale apps and WireGuard tunnels. Headscale serves one tailnet and lacks some hosted features, such as Funnel, Serve and network flow logs.

Can Headscale be used for personal use?

Yes. Headscale's stated design goal is a single tailnet for personal use or a small open-source organization, and it is released under the BSD-3-Clause license.

Does Headscale have a web UI?

No. Headscale is managed with the headscale command line tool. Its documentation lists community web interfaces, such as Headplane and headscale-ui, which the Headscale authors do not maintain.

Can I run Headscale with Docker Compose?

Yes, with the container images Headscale publishes. Its community-written container page includes a Docker Compose example, but Headscale's FAQ says Docker deployments are not officially supported. This tutorial uses the DEB package, which Headscale recommends on Ubuntu.

Which ports does Headscale use?

Headscale serves Tailscale clients on 443/tcp. It needs 80/tcp only for the built-in Let's Encrypt HTTP-01 check and 3478/udp only for the embedded DERP relay. gRPC on 50443/tcp and metrics on 9090/tcp stay on 127.0.0.1 by default.

What is the difference between Headscale and WireGuard?

WireGuard is the VPN protocol that carries the traffic. Headscale is a control server that hands out WireGuard keys and addresses to Tailscale clients, so devices find each other without hand-written peer configs.

Can the Headscale server also be a Tailscale exit node?

Headscale's FAQ says running Headscale and the Tailscale client on the same machine can cause problems with subnet routers, relays and MagicDNS, and is not supported. Run the exit node on a second server and approve it with headscale nodes approve-routes.

Next steps

Arct Cloud Cost Optimized plans run on AMD EPYC Milan 7003 Series processors up to 3.7 GHz with NVMe storage. Compare plans.

This work is licensed under CC BY-NC-SA 4.0.