VPNSmith
Free tool - Headscale server

Headscale config generator

Enter your domain, pick Docker or the .deb package and how HTTPS is handled: the generator writes config.yaml, the compose file or install script, the reverse proxy file, an optional exit node policy and the commands to create your first user and connect a device. Built from the official files of Headscale 0.29.5. Nothing leaves your browser.

Everything is generated in your browser. Nothing is sent or stored.

A DNS name whose A (and AAAA) record points to the VPS, for example headscale.example.com.

Devices become name.base-domain. Must be a different domain from the one above; it does not need to exist publicly.

Created with headscale users create. Also used as tag owner in the exit node policy.

Used by certbot or Headscale for expiry notices. Caddy works without it.

Install method
HTTPS

Caddy is the shortest path. Choose nginx if it already runs on the server, built-in TLS if the VPS only runs Headscale.

Upstream DNS for MagicDNS
Options
  • Create the DNS record of the domain before starting: the certificate request fails if the name does not point to the VPS yet.

Generated for https://headscale.example.com · Headscale 0.29.5 · 6 files

/opt/headscale/config/config.yaml
---
# Headscale 0.29.5: /etc/headscale/config.yaml
server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
trusted_proxies: []
noise:
  private_key_path: /var/lib/headscale/noise_private.key
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48
  allocation: sequential
derp:
  server:
    enabled: false
    region_id: 999
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    verify_clients: true
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key
    automatically_add_embedded_derp_region: true
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  paths: []
  auto_update_enabled: true
  update_frequency: 3h
disable_check_updates: false
node:
  expiry: 0
  ephemeral:
    inactivity_timeout: 30m
database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite
    write_ahead_log: true
    wal_autocheckpoint: 1000
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: ""
tls_letsencrypt_hostname: ""
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""
log:
  level: info
  format: text
policy:
  mode: file
  path: "/etc/headscale/policy.json"
dns:
  magic_dns: true
  base_domain: ts.example.com
  override_local_dns: true
  nameservers:
    global:
      - 1.1.1.1
      - 1.0.0.1
      - 2606:4700:4700::1111
      - 2606:4700:4700::1001
    split: {}
  search_domains: []
  extra_records: []
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
  enabled: false
taildrop:
  enabled: true
/opt/headscale/docker-compose.yaml
services:
  headscale:
    image: docker.io/headscale/headscale:0.29.5
    container_name: headscale
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /var/run/headscale
    ports:
      - "127.0.0.1:8080:8080"
      - "127.0.0.1:9090:9090"
    volumes:
      - /opt/headscale/config:/etc/headscale:ro
      - /opt/headscale/lib:/var/lib/headscale
    command: serve
    healthcheck:
      test: ["CMD", "headscale", "health"]

  caddy:
    image: docker.io/library/caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - /opt/headscale/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - headscale

volumes:
  caddy_data:
  caddy_config:
/opt/headscale/Caddyfile
http://headscale.example.com {
	# Tailscale captive portal detection
	handle /generate_204 {
		respond 204
	}

	handle * {
		redir https://{host}{uri}
	}
}

headscale.example.com {
	reverse_proxy headscale:8080 {
		header_up True-Client-IP {remote_host}
		header_up X-Real-IP {remote_host}
	}
}
/opt/headscale/config/policy.json
{
  "tagOwners": {
    "tag:exit": [
      "admin@"
    ]
  },
  "autoApprovers": {
    "exitNode": [
      "tag:exit"
    ]
  }
}
setup-headscale.sh
#!/usr/bin/env bash
set -euo pipefail

# 1. Install Docker if missing (official script)
command -v docker >/dev/null || curl -fsSL https://get.docker.com | sudo sh

# 2. Folders for the configuration and the data
sudo mkdir -p /opt/headscale/config /opt/headscale/lib
cd /opt/headscale

# 3. Save the generated files at these paths first
for f in config/config.yaml docker-compose.yaml Caddyfile config/policy.json; do
  [ -f "$f" ] || { echo "Missing /opt/headscale/$f"; exit 1; }
done

# 4. Start Headscale
sudo docker compose up -d
sleep 5

# 5. Check: must print a JSON health status
curl -fsS http://127.0.0.1:8080/health
first-steps.sh
# Create the first user
sudo docker exec headscale headscale users create admin

# Note the numeric ID of the user
sudo docker exec headscale headscale users list

# Create a pre-auth key (valid 1 hour, single use by default)
sudo docker exec headscale headscale preauthkeys create --user <USER_ID>

# On each device: log in to your Headscale server
sudo tailscale up --login-server https://headscale.example.com --authkey <YOUR_AUTH_KEY>

# On the exit node VPS: log in with the tag so it is auto-approved
sudo tailscale up --login-server https://headscale.example.com --authkey <YOUR_AUTH_KEY> \
  --advertise-exit-node --advertise-tags=tag:exit

# After editing policy.json: reload Headscale
sudo docker kill --signal=HUP headscale

# Browser login instead: approve the device with the Auth ID it shows
sudo docker exec headscale headscale auth register --user admin --auth-id <AUTH_ID>

# List the connected devices
sudo docker exec headscale headscale nodes list

Headscale needs a server that stays online

A small VPS with a public IPv4 and IPv6 runs these files as they are, and the same machine can serve as the exit node of your tailnet.

Affiliate link: we may earn a commission at no extra cost to you. The generator stays free and does not depend on it.

See the Contabo VPS

What the generated files do

Headscale is the open source replacement for the Tailscale control server: your devices keep the official Tailscale apps, but they log in to your own server with --login-server. It needs three things to work: a public HTTPS address on port 443 that matches server_url, a MagicDNS base_domain that is different from that address, and a reverse proxy that passes WebSocket upgrades if you do not let Headscale handle TLS itself.

The config.yaml is the full example file of version 0.29.5 with your values filled in, so every key is present and nothing relies on an undocumented default. In Docker the container listens on 0.0.0.0 inside its own network and the ports are only published on 127.0.0.1, except 80 and 443 when Headscale terminates TLS. Behind a reverse proxy on the same host, trusted_proxies is set to localhost so the real client IPs appear in the logs.

Several commands changed in recent releases, which is why many tutorials fail: pre-auth keys are now created with the numeric user ID (headscale users list shows it), and a device that logs in through the browser is approved with headscale auth register and its Auth ID. The first steps block uses the 0.29 syntax.

Caddy, nginx or built-in TLS?

Three ways to serve Headscale over HTTPS. All three are documented upstream; the right one depends on what else runs on the server.

HTTPS handled byCertificateBest whenWatch out
Caddy (reverse proxy)Automatic, renewed by CaddyYou want the shortest working setup or other sites on the same VPSPorts 80 and 443 must reach Caddy; WebSocket works with no extra line
nginx (reverse proxy)certbot, renewed by its timernginx already serves other sites on the serverThe Upgrade and Connection headers are mandatory, or clients never connect
Headscale built-in Let's EncryptAutomatic, renewed by HeadscaleThe VPS only runs HeadscaleHeadscale takes ports 80 and 443 for itself; no other web service on that IP

Frequently asked questions

What is the minimum config.yaml for Headscale?
Headscale reads /etc/headscale/config.yaml. The values you must change are server_url (your public HTTPS address), listen_addr, and dns.base_domain, which must be a different domain from server_url. The safest start is the example file of your exact version with those values edited, which is what this generator produces, because keys are renamed between releases.
Why does tailscale up hang or fail with my Headscale server?
In most cases the reverse proxy does not pass the WebSocket upgrade (nginx without the Upgrade and Connection headers), server_url does not match the address clients use, or the certificate is not trusted. Check curl https://your-domain/health from outside the server, then the Headscale logs. Serving Headscale on a port other than 443 is possible but the Tailscale client assumes 443 in some cases.
How do I create a pre-auth key in Headscale 0.29?
Create a user with headscale users create <name>, read its numeric ID with headscale users list, then run headscale preauthkeys create --user <ID>. By default the key is valid for one hour and can be used once. On the device: tailscale up --login-server https://your-domain --authkey <key>.
Do I need the embedded DERP server?
No. By default Headscale gives clients the public DERP relays run by Tailscale, which are used only when a direct connection is impossible. Enable the embedded DERP if you want relays under your control: it needs UDP 3478 open and the public IPv4 (and IPv6 if any) of the server in the config.
How big a VPS does Headscale need?
Very little: the control server only coordinates keys and routes, traffic goes directly between devices. The smallest plans are enough for a home or a small team. What matters is a public IPv4, ideally IPv6 too, and a provider that leaves ports 80, 443 and 3478 open. The same server can also be your exit node.