How to Self-Host Headscale on Ubuntu and Connect Linux, macOS, and Mobile Nodes

Updated on Aug 24, 2026
Mila H
7 MINS READ
Table of Contents
Self-Host Headscale on Ubuntu

Headscale is an open-source and self-hosted version of the Tailscale control server. It lets you manage your own private mesh VPN network and connect devices like Linux, macOS, Android, and iPhone through your own server. In this guide, we will show you how to self-host Headscale on Ubuntu and add clients.

What Headscale Does

Headscale gives you your own control plane for a private WireGuard mesh, so your devices can join the same network without using the default hosted control server.

It is recommended to use the DEB package path on Ubuntu because it creates a local service user, ships a default config file, and includes a systemd service file.

If you want a stable setup for remote access, self-host Headscale on Ubuntu on a VPS instead of leaving the control plane behind a home router that may change IPs or go offline. A VPS is usually a better place for the control server because it is public, always on, and easier to protect with a reverse proxy and TLS. That is why many people self-host Headscale on Ubuntu on a Linux VPS instead of a home lab box that depends on home internet uptime.

For a reliable server base, you can use a Linux VPS so your Headscale URL stays reachable all the time.

What You Need to Self-Host Headscale on Ubuntu

For this setup, you need a server running Ubuntu 22.04 or 24.04 because these versions work well with Headscale. You also need a domain name, and it should point to your server’s public IP address.

To keep the setup secure, put Nginx or Caddy in front of Headscale and use HTTPS for the public connection, since clients connect to it through a secure URL.

For a small or personal deployment, SQLite is a good choice because it is simple and easy to manage.

It is also a good idea to keep Headscale listening only on localhost and let the reverse proxy handle public traffic.

Finally, make sure ports 80 and 443 are open so the reverse proxy can serve HTTP and HTTPS traffic properly.

If you later want to publish a private app behind your VPN, read this guide on Cloudflare Tunnel for that use case.

Now proceed to the following steps to complete the setup.

Method 1. Install Headscale on Ubuntu with DEB Package

To self-host Headscale on Ubuntu, it is recommended to use the DEB package method. For installing the package, you can use the commands below:

Bash
sudo apt updatesudo apt install wget curl gnupg -y wget -O /tmp/headscale.deb \https://github.com/juanfont/headscale/releases/download/v0.28.0/headscale_0.28.0_linux_amd64.deb sudo apt install ./headscale.deb -y

After installation, Headscale already has a systemd unit and a default config path, which is one reason many admins self-host Headscale on Ubuntu this way instead of building the service manually.

Headscale Config File

The main config file is located under /etc/headscale/config.yaml, and it includes an up-to-date example at /usr/share/doc/headscale/examples/config-example.yaml.

When you self-host Headscale on Ubuntu, start with the shipped example for your installed version, then edit only the values that apply to your server.

Edit the config file:

Bash
sudo nano /etc/headscale/config.yaml

A practical minimal layout looks like this:

YAML
server_url: https://hs.example.com listen_addr: 127.0.0.1:8080metrics_listen_addr: 127.0.0.1:9090 noise:  private_key_path: /var/lib/headscale/noise_private.key prefixes:  v4: 100.64.0.0/10  v6: fd7a:115c:a1e0::/48 database:  type: sqlite  sqlite:    path: /var/lib/headscale/db.sqlite dns:  base_domain: headscale.internal unix_socket: /var/run/headscale/headscale.sock

Use your own public domain in server_url, keep listen_addr on 127.0.0.1:8080, and store the SQLite database under /var/lib/headscale/ so the service keeps its state locally.

Start and Verify the Headscale Service

After configuration changes, restart the service and verify it with the commands below:

Bash
sudo systemctl restart headscalesudo systemctl enable --now headscalesudo systemctl status headscale

Start and Verify the Headscale Service

You can also check basic health through the local service endpoint once the reverse proxy is in place or by testing the local port first.

A simple local check is:

Bash
curl http://127.0.0.1:8080/health

In the output, you must see:

JSON
{  "status": "pass"}

Put TLS in front of Headscale

Headscale works best behind a reverse proxy because your clients should connect to a clean HTTPS address rather than a local port. Open ports 80 and 443 on the firewall, keep Headscale on localhost, and let Nginx handle the public traffic.

We add HTTPS with Nginx. Use the commands below to install Nginx and Certbot:

Bash
sudo apt updatesudo apt install nginx certbot python3-certbot-nginx -y

Allow required firewall ports through the UFW firewall:

Bash
sudo ufw allow OpenSSHsudo ufw allow 80/tcpsudo ufw allow 443/tcpsudo ufw allow 3478/udpsudo ufw enablesudo ufw status

Then, make sure Headscale listens locally. Edit your config file:

Bash
sudo nano /etc/headscale/config.yaml

Use values like these:

YAML
server_url: https://hs.example.comlisten_addr: 127.0.0.1:8080metrics_listen_addr: 127.0.0.1:9090tls_cert_path: ""tls_key_path: ""

This keeps Headscale on localhost and lets Nginx handle TLS in front of it.

Create the Nginx site file:

Bash
sudo nano /etc/nginx/sites-available/headscale

Paste this config:

Bash
map $http_upgrade $connection_upgrade {    default upgrade;    '' close;} server {    listen 80;    listen [::]:80;    server_name hs.example.com;     location / {        proxy_pass http://127.0.0.1:8080;        proxy_http_version 1.1;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection $connection_upgrade;        proxy_set_header Host $server_name;        proxy_redirect http:// https://;        proxy_buffering off;        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;    }}

Enable the site and test Nginx:

Bash
sudo ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/headscalesudo nginx -tsudo systemctl reload nginx

Request the SSL certificate:

Bash
sudo certbot --nginx -d hs.example.com

Certbot will request the certificate and usually update the Nginx server block for HTTPS automatically. After this, Nginx will serve your Headscale URL over HTTPS.

Once you are done, you can check everything with the following commands:

Bash
curl http://127.0.0.1:8080/healthcurl -I http://hs.example.comcurl -I https://hs.example.comsudo systemctl status nginxsudo systemctl status headscale

Create a User and Join Nodes

After the service starts, you can self-host Headscale on Ubuntu for a small team or personal setup by creating one user and then making a pre-auth key for node enrollment.

Bash
headscale users create mainheadscale users listheadscale preauthkeys create --user 1 --reusable --expiration 24h

Connect Linux, macOS, and Mobile Clients

Next, self-hosting Headscale on Ubuntu becomes useful only after your devices join the network. You must install the Tailscale app on each device and point it to your own control server URL.

You can self-host Headscale on Ubuntu and attach a Linux machine with a pre-auth key like this:

Bash
sudo tailscale up \  --login-server https://hs.example.com \  --authkey YOUR_PREAUTH_KEY

You can also connect a Mac by using the Tailscale client with a custom control server URL, either from the app settings or the command line.

Bash
tailscale login --login-server https://hs.example.com

For phones, self-host Headscale on Ubuntu works best when your server has a valid HTTPS setup and a stable public domain, because the mobile app needs to reach that custom control server from any network.
After login, check that the node appears in Headscale and then test access between two joined devices.

Method 2. Headscale Docker Setup

If Docker is already installed, you can run Headscale in a container instead of using the package method. The container setup is simple, but your config file must include a DERP map and a safe DNS block, or the service may fail to start on newer Headscale versions.

First, create the folders for the config and database files:

Bash
mkdir -p ~/headscale/{config,lib}cd ~/headscale

Next, create the main config file:

Bash
nano ~/headscale/config/config.yaml

Paste this working example:

YAML
server_url: https://hs.example.comlisten_addr: 0.0.0.0:8080metrics_listen_addr: 0.0.0.0:9090 noise:  private_key_path: /var/lib/headscale/noise_private.key prefixes:  v4: 100.64.0.0/10  v6: fd7a:115c:a1e0::/48 derp:  server:    enabled: false  urls:    - https://controlplane.tailscale.com/derpmap/default  paths: []  auto_update_enabled: true  update_frequency: 24h database:  type: sqlite  sqlite:    path: /var/lib/headscale/db.sqlite dns:  base_domain: headscale.internal  override_local_dns: false unix_socket: /var/run/headscale/headscale.sock

This config uses SQLite for storage, disables forced DNS override, and loads the default DERP map so Headscale can start cleanly.

Now start the container:

Bash
docker run \--name headscale \--detach \--restart unless-stopped \--read-only \--tmpfs /var/run/headscale \--volume "$(pwd)/config:/etc/headscale:ro" \--volume "$(pwd)/lib:/var/lib/headscale" \--publish 127.0.0.1:8080:8080 \--publish 127.0.0.1:9090:9090 \--health-cmd "CMD headscale health" \docker.io/headscale/headscale:0.28.0 \serve

This binds Headscale to localhost on the host machine, which is the right setup when you want to place Nginx or another reverse proxy in front of it.

After that, check that the container is healthy:

Bash
docker psdocker logs --tail 50 headscalecurl http://127.0.0.1:8080/health

Check Headscale Container status

If the health check works, Headscale is running and ready for the reverse proxy and client enrollment steps.

Conclusion

Headscale is a simple way to run your own private mesh control server with Tailscale clients. With Ubuntu, SQLite, systemd, a reverse proxy, and a public domain, you can build a clean setup that is easy to manage and easy to grow later.

We hope you enjoy this guide. For more detailed information, you can check the official Headscale docs.

Yes, you should use HTTPS because clients connect through a secure control server URL.

Yes, SQLite is a good choice for a small setup and keeps the first install simple.

A VPS gives your control server a stable public IP, better uptime, and simpler firewall rules, which makes it a better option for Headscale than a typical home connection.