//------------------------------------------------------------------- //-------------------------------------------------------------------
Self-Host Headscale on Ubuntu

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

Headscale is an open-source, 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 instead of Tailscale’s hosted control plane. In this guide, we will show you how to self-host Headscale on Ubuntu with the package-based method first, then a Docker container option, and add Linux, macOS, and mobile 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:

sudo apt update
sudo 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:

sudo nano /etc/headscale/config.yaml

A practical minimal layout looks like this:

server_url: https://hs.example.com

listen_addr: 127.0.0.1:8080
metrics_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:

sudo systemctl restart headscale
sudo systemctl enable --now headscale
sudo 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:

curl http://127.0.0.1:8080/health

In the output, you must see:

{"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:

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx -y

Allow required firewall ports through the UFW firewall:

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

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

sudo nano /etc/headscale/config.yaml

Use values like these:

server_url: https://hs.example.com
listen_addr: 127.0.0.1:8080
metrics_listen_addr: 127.0.0.1:9090
tls_cert_path: ""
tls_key_path: ""

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

Create the Nginx site file:

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

Paste this config:

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:

sudo ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/headscale
sudo nginx -t
sudo systemctl reload nginx

Request the SSL certificate:

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:

curl http://127.0.0.1:8080/health
curl -I http://hs.example.com
curl -I https://hs.example.com
sudo systemctl status nginx
sudo 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.

headscale users create main
headscale users list
headscale 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:

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.

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:

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

Next, create the main config file:

nano ~/headscale/config/config.yaml

Paste this working example:

server_url: https://hs.example.com
listen_addr: 0.0.0.0:8080
metrics_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:

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:

docker ps
docker logs --tail 50 headscale
curl 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.

FAQs

Do I need HTTPS for Headscale setup?

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

Is SQLite enough for Headscale?

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

Why is a VPS better than home internet for Headscale?

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.

Post Your Comment

PerLod delivers high-performance hosting with real-time support and unmatched reliability.

Contact us

Payment methods

payment gateway