Tailscale is an open-source VPN. It works very well. However, its control server is still on their systems. Headscale is an open-source, self-hosted implementation of the Tailscale control server.

Here, I’ll list down the steps required to install Headscale on Ubuntu.

Download the correct Headscale version

  1. get the latest release number from Github Releases Page
  2. get system architecture (for Linux, amd64)

Use the following command to get the correct version. Substitute the version number from 1 above and the system architecture from 2.

HEADSCALE_VERSION="0.27.1"
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"

Install the Headscale server

sudo apt install ./headscale.deb

Modify config.yaml

The Headscale config.yaml is available in /etc/headscale/config.yaml

The following variables need to be set there:

server_urllisten_addrmetrics_listen_addr

Since, the headscale server will be running behind a proxy, these variables will point to 127.0.01

server_url: http://127.0.0.1:8081

listen_addr: 127.0.0.1:8081

metrics_listen_addr: 127.0.0.1:9091

Start the Headscale server

sudo systemctl start headscale

Install and run Headscale UI

Now, that Headscale has been set up, a UI is needed to be able to add the clients. For this, I used Headscale UI, and open-source front-end for the the Headscale control server.

Headscale UI docker-compose.yml file

services:
  headscale-ui:
    container_name: headscale-ui
    image: ghcr.io/gurucomputing/headscale-ui:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:9080:8443"

Nginx Proxy

Nginx sits in front of both Headscale and Headscale UI. It handles the SSL certs from LetsEncrypt.

map $http_upgrade $connection_upgrade {
  default keep-alive;
  'websocket' upgrade;
  ''     close;
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;

  ssl_certificate /path/to/cert;
  ssl_certificate_key /path/to/key;

  server_name headscale.your-domain.com;
  server_tokens off;

  access_log  /var/log/nginx/headscale_access.log;
  error_log /var/log/nginx/headscale_error.log;

  # Headscale UI (gurucomputing) – e.g. https://headscale.your-domain.com/web
  # DON'T USE /admin as that is expected by the headscale app and will not work
  location /web {
    proxy_pass https://127.0.0.1:9080;
    proxy_http_version 1.1;
    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_set_header X-Forwarded-Proto $scheme;
  }

  # Headscale API and control – everything else
  location / {
    proxy_pass http://127.0.0.1:8081/;
    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_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 86400;
  }
}

server {
  listen 80;
  listen [::]:80;
  server_name headscale.your-domain.com;
  server_tokens off;
  return 301 https://$host$request_uri;
}

Test and reload Nginx

nginx -t
nginx -s reload

This sets up both Headscale and Headscale UI