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.
Caddy is the shortest path. Choose nginx if it already runs on the server, built-in TLS if the VPS only runs Headscale.
- 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
---
# 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
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:
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}
}
}
{
"tagOwners": {
"tag:exit": [
"admin@"
]
},
"autoApprovers": {
"exitNode": [
"tag:exit"
]
}
}
#!/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
# 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.
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 by | Certificate | Best when | Watch out |
|---|---|---|---|
| Caddy (reverse proxy) | Automatic, renewed by Caddy | You want the shortest working setup or other sites on the same VPS | Ports 80 and 443 must reach Caddy; WebSocket works with no extra line |
| nginx (reverse proxy) | certbot, renewed by its timer | nginx already serves other sites on the server | The Upgrade and Connection headers are mandatory, or clients never connect |
| Headscale built-in Let's Encrypt | Automatic, renewed by Headscale | The VPS only runs Headscale | Headscale takes ports 80 and 443 for itself; no other web service on that IP |