Self-Hosted Tailscale with Headscale + Headplane (2026 Guide)
This guide shows how to set up your own private Tailscale control server using Headscale (open-source alternative to Tailscale’s coordination server) and Headplane (nice web UI for managing it).
Everything runs in Docker, works behind Nginx reverse proxy with HTTPS, and is perfect for a small private network (e.g. you + your devices + friends).
Tested on a cheap VPS (Ubuntu 24.04), but works on any Linux server with Docker.
Prerequisites
- VPS or home server with public IP (Hetzner, DigitalOcean, etc.)
- Domain (e.g.
net.kurays.dev) with A-record pointing to your server IP - Docker + docker-compose installed
- Nginx installed on host (for reverse proxy + Let’s Encrypt)
Step 1: Initial structure
mkdir ~/tailscale && cd ~/tailscale
This folder will contain all necessary configs.
Ensure that you changed all “your.domain” in configs to your actual domain name.
docker-compose.yml
version: '3.8'
services:
headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
command: serve
volumes:
- ./config.yaml:/etc/headscale/config.yaml:ro
- ./data:/var/lib/headscale
networks:
- tailnet
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
volumes:
- ./headplane-config.yaml:/etc/headplane/config.yaml:ro
- ./headplane-data:/var/lib/headplane
networks:
- tailnet
networks:
tailnet:
driver: bridge
config.yaml (Main headscale config)
server_url: https://your.domain
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1: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
prefixes:
v4: 100.64.0.0/10
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
derp:
server:
enabled: false
stun_listen_addr: "0.0.0.0:3478"
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: true
dns:
magic_dns: true
base_domain: your.domain
override_local_dns: true
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
search_domains: []
extra_records: []
split_dns: []
log:
level: info
headplane-config.yaml
server:
host: 0.0.0.0
port: 3000
cookie_secure: true
cookie_secret: "SoMeRaNdOmStRiNgflepfswgoe" # openssl rand -base64 32
headscale:
url: http://headscale:8080
api_key: "" # fill later
config_strict: false
/etc/nginx/sites-avaiable/your.domain
server {
listen 80;
server_name your.domain;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name your.domain;
ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /admin {
proxy_pass http://127.0.0.1:3000/;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Enable configs by symlinking it to sites-enabled and generate certs with certbot. And restart nginx
sudo ln -s /etc/nginx/sites-available/your.domain /etc/nginx/sites-enabled/
sudo certbot --nginx -d your.domain
sudo nginx -t && sudo systemctl reload nginx
First launch
After all configuration you can run
docker compose up -d
And check docker compose logs for any error.
After first launch your folder should look like this
tailscale ├── docker-compose.yml ├── config.yaml ├── headplane-config.yaml ├── data └── headplane-data
If all started correctly you can request api key for admin panel
docker exec -it headscale headscale apikeys create --expiration 365d
Copy the key and paste into headplane-config.yaml to api_key: “” Restart headplane:
docker compose restart headplane
To check open: https://your.domain/admin - headplane dashboard
In Headplane: Users → create user Pre-auth keys → create key
On client:
tailscale up --login-server=https://net.kurays.dev --authkey=your-pre-auth-key-here
Now you can fully enjoy your very own Tailnet with own coordination server.
Possibilities:
MagicDNS: your.doman (devices become e.g. hostname.your.domain) You can enter even non existed domain inside Tailnet it will work.
Subnet routes: advertise your home LAN and approve in Headplane. You can install tailscale on your OpenWRT routes and use it to pass entire homelab into tailnet and access it everywhere.
Exit node: advertise on VPS and approve the use as exit node in Headplane. You can use your home node as exit in public WiFi or even in another country.