Overview

Headscale is an opensource reverse-engineered implementation of the closed source Tailscale coordination server. There are many advantages to using the original Tailscale coordination server, such as a convenient admin panel and multiple “tailnets” - separate VPNs you can quickly switch between. However, I am on a quest to explore opensource and privacy-focused software, I’ve decided to set up Headscale as my Tailscale coordination server.

This guide outlines how to set up Headscale running in a Docker container behind a Traefik reverse proxy. It uses a free Ubuntu VPS from the Oracle Cloud Free Tier, but any linux-based host with a public IP and about ~1GB of memory should work for a personal VPN.

Setting up Headscale behind a reverse-proxy is not something that the maintainers support or use themselves, but it is a feature that is often requested by community members. I’ve figured out how to run Headscale behind Traefik. The following is my working prototype.

Prerequisites

VPS Setup

  1. Create a free VPS with Oracle Cloud.
  2. Point a hostname to the public IP of your VPS (e.g. headscale.example.com).
  3. Update packages and install docker and docker-compose.
  4. Set up a container running Traefik as described in this guide.

Headscale Setup

  1. Login to the VPS with SSH.
  2. Create a directory for Headscale: mkdir /srv/apps/headscale
  3. Change to that directory: cd /srv/apps/headscale
  4. Create a compose.yml file and paste the following into, updating values as required:
services:
  headscale:
    image: headscale/headscale:latest
    container_name: headscale
    hostname: headscale
    labels:
      - traefik.enable=true
      - traefik.http.routers.headscale-example-com.tls=true
      - traefik.http.routers.headscale-example-com.rule=Host(`headscale.example.com`)
      - traefik.http.routers.headscale-example-com.service=headscale-example-com
      - traefik.http.services.headscale-example-com.loadbalancer.server.port=8080
    ports:
      - 3478:3478/udp # have not needed to open in Oracle VPC firewall so far
    volumes:
      - ./data/headscale:/var/lib/headscale
      - ./data/config:/etc/headscale
    networks:
      - proxy
    restart: unless-stopped
    command: "headscale serve"

networks:
  proxy:
    external: true
  1. Create the config directory: mkdir -p data/config
  2. Create the sqlite db: touch data/config/db.sqlite
  3. Create and edit the config file: vi data/config/config.yaml (.yaml not .yml seems to matter)
  4. Paste in the configuration below, updating values as required:
---
server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 0.0.0.0:50443
grpc_allow_insecure: false
private_key_path: /var/lib/headscale/private.key
noise:
  private_key_path: /var/lib/headscale/noise_private.key
ip_prefixes:
  - fd7a:115c:a1e0::/48
  - 100.64.0.0/10
derp:
  server:
    enabled: false
    region_id: 999
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    stun_listen_addr: "0.0.0.0:3478"
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  paths: []
  auto_update_enabled: true
  update_frequency: 24h
disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
db_type: sqlite3
db_path: /etc/headscale/db.sqlite
acme_url: https://acme-v02.api.letsencrypt.org/directory
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""
log:
  format: text
  level: info
acl_policy_path: ""
dns_config:
  override_local_dns: true
  nameservers:
    - 1.1.1.1
    - 1.0.0.1
  domains: []
  magic_dns: true
  base_domain: example.com
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
  enabled: false
randomize_client_port: false

Note: The full Headscale configuration with comments and default values can be found on their Github. This original will likely be useful when debugging.

Connecting Tailscale Nodes

This process is fairly straight forward and based on the Headscale documentation. The only caveat is that you need to prefix the tailscale commands with docker exec [tailscale_container]...

Troubleshooting

It took me a little bit of testing to arrive at the combination of URL and listening address. In the end, I found that the following worked well. Initially I was trying to use port 443 (which some other tutorials showed) and received errors like Client sent an HTTP request to an HTTPS server.

server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080

It was important to leave the tls_cert_path and tls_key_path empty, as discussed on the Headscale Github.

acme_url: https://acme-v02.api.letsencrypt.org/directory
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""

If you have suggestions or would like another pair of eyes to toubleshoot, please reach out by opening an issue in the repo that tracks this website. You can find it here:

https://github.com/radarsymphony/radarsymphony.github.io/issues

Resources