Integrating Headscale and Headplane with Pangolin
This comprehensive guide will walk you through setting up Headscale (an open-source Tailscale control server) and Headplane (a web UI for Headscale) alongside your existing Pangolin installation. By the end of this guide, you’ll have a fully functional mesh VPN solution integrated with your Pangolin reverse proxy.
Understanding the Architecture
High-Level Architecture
┌─────────────┐
│ │
│ Traefik │
│ │
└──────┬──────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌────────▼───────┐ ┌────────▼───────┐ ┌────────▼───────┐
│ │ │ │ │ │
│ Pangolin │ │ Headscale │ │ Headplane │
│ │ │ │ │ │
└────────────────┘ └────────────────┘ └────────────────┘
Before diving into the setup, let’s understand how these components work together:
- Pangolin - Self-hosted tunneled reverse proxy with identity management
- Headscale - Open-source implementation of the Tailscale control server
- Headplane - Web UI for managing Headscale
- Traefik - The routing layer that connects everything together
The integration uses Pangolin’s existing Traefik instance to route traffic to both Pangolin and Headscale/Headplane services. This provides a unified management experience for both systems.
Prerequisites
- A working Pangolin installation with Traefik
- Docker and Docker Compose
- A domain for Headscale (we’ll use
heads.intranet.hhf.technology
) - Basic familiarity with Docker networking and Traefik
You can deploy traefik separately for headscale also but more complicated. Let me know if you need that. But this is for broder crowd adaptation.
Part 1: Directory Structure Setup
First, let’s create the necessary directory structure:
# Navigate to your Pangolin installation directory
cd /path/to/pangolin
# Create required directories
mkdir -p headscale/config headscale/data headplane
Part 2: Headscale Configuration
Create the Headscale configuration file:
nano headscale/config/config.yaml
Add the following configuration:
server_url: https://heads.intranet.hhf.technology
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
# Required prefixes section
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
# DERP configuration
derp:
server:
enabled: false
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: true
update_frequency: 24h
# DNS settings
dns:
magic_dns: true
nameservers:
global:
- 1.1.1.1
- 8.8.8.8
base_domain: ts.hhf.technology
# Database settings
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
# Noise key settings
noise:
private_key_path: /var/lib/headscale/noise_private.key
# Unix socket
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
log:
format: text
level: info
This configuration:
- Points to your domain (
heads.intranet.hhf.technology
) - Sets up the IP ranges for your Tailscale network
- Configures DNS for your VPN
- Sets up storage for the SQLite database
- Configures logging and other essential parameters
Part 3: Headplane Configuration
Create the Headplane configuration file:
nano headplane/config.yaml
Add the following configuration:
server:
host: "0.0.0.0"
port: 3000
cookie_secret: "GENERATE_A_STRONG_SECRET" # Change this!
cookie_secure: true
headscale:
url: "http://headscale:8080"
config_path: "/etc/headscale/config.yaml"
config_strict: true
# Integration configurations
integration:
docker:
enabled: true
container_name: "headscale"
socket: "unix:///var/run/docker.sock"
kubernetes:
enabled: false
validate_manifest: true
pod_name: "headscale"
proc:
enabled: false
Generate a strong cookie secret:
openssl rand -hex 16
Replace GENERATE_A_STRONG_SECRET
with the generated value. This secret is used to encrypt session cookies.
Part 4: Traefik Configuration
Let’s update the Traefik configuration to route traffic to Headscale and Headplane.
Create or modify your dynamic configuration file:
nano config/traefik/dynamic_config.yml
Replace its contents with:
http:
middlewares:
# Existing middlewares
redirect-to-https:
redirectScheme:
scheme: https
# Root path redirect middleware for Headscale
headscale-redirect:
redirectRegex:
regex: "^(https?://[^/]+)/?$"
replacement: "${1}/admin"
permanent: true
# Admin path rewriting middleware for Headplane
headscale-rewrite-admin:
addPrefix:
prefix: "/admin"
# CORS middleware for Headscale API
cors:
headers:
accessControlAllowMethods:
- GET
- OPTIONS
- PUT
- POST
accessControlAllowHeaders:
- "*"
accessControlAllowOriginList:
- "https://heads.intranet.hhf.technology"
accessControlMaxAge: 100
addVaryHeader: true
# Basic auth middleware for Traefik dashboard (optional)
traefik-dashboard-auth:
basicAuth:
users:
# Generate this with: htpasswd -nb admin YOUR_PASSWORD
- "admin:$apr1$ls1hhnt/$fKLs2zmr51n8RBDlw.MlG."
routers:
# Your existing Pangolin routers here
main-app-router-redirect:
rule: "Host(`pangolin.development.hhf.technology`)"
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
next-router:
rule: "Host(`pangolin.development.hhf.technology`) && !PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
api-router:
rule: "Host(`pangolin.development.hhf.technology`) && PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
ws-router:
rule: "Host(`pangolin.development.hhf.technology`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# Headscale API router - for all non-admin paths
headscale-api-rtr:
rule: "Host(`heads.intranet.hhf.technology`) && !PathPrefix(`/admin`)"
service: headscale-direct-svc
entryPoints:
- websecure
tls:
certResolver: letsencrypt
middlewares:
- cors
# Headplane admin interface router
headplane-admin-rtr:
rule: "Host(`heads.intranet.hhf.technology`) && PathPrefix(`/admin`)"
service: headplane-direct-svc
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# Root path redirection router
headscale-root-rtr:
rule: "Host(`heads.intranet.hhf.technology`) && Path(`/`)"
service: noop@internal
entryPoints:
- websecure
tls:
certResolver: letsencrypt
middlewares:
- headscale-redirect
priority: 100 # Make this higher priority than other rules
# Tailscale subdomain handler
headscale-ts-rtr:
rule: "HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.ts.hhf.technology`)"
service: headscale-direct-svc
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# Traefik dashboard access (optional)
traefik-dashboard-rtr:
rule: "Host(`traefik.development.hhf.technology`)"
service: api@internal
entryPoints:
- websecure
tls:
certResolver: letsencrypt
middlewares:
- traefik-dashboard-auth
services:
# Your existing Pangolin services
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002" # Next.js server
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000" # API/WebSocket server
# Headscale services
headscale-direct-svc:
loadBalancer:
servers:
- url: "http://headscale:8080" # Points directly to Headscale container
headplane-direct-svc:
loadBalancer:
servers:
- url: "http://headplane:3000" # Points directly to Headplane container
headplane-redirect-svc:
loadBalancer:
servers:
- url: "http://headplane:3000" # For root redirect to admin
serversTransports:
insecureTransport:
insecureSkipVerify: true
This configuration:
- Sets up middlewares for redirection, CORS, and authentication
- Creates routers for Headscale API and Headplane UI
- Implements a special root path redirect to the admin interface
- Configures services to direct traffic to the appropriate containers
Part 5: Create Docker Compose File for Headscale
Create a new Docker Compose file specifically for Headscale and Headplane:
nano docker-compose.headscale.yml
Add the following content:
services:
headscale:
image: headscale/headscale:0.25.1
container_name: headscale
restart: unless-stopped
command: serve
volumes:
- ./headscale/config:/etc/headscale
- ./headscale/data:/var/lib/headscale
networks:
- pangolin
# No Traefik labels needed - we're routing through main Traefik
headplane:
image: ghcr.io/tale/headplane:0.5.1
container_name: headplane
restart: unless-stopped
volumes:
- ./headplane/config.yaml:/etc/headplane/config.yaml
- ./headscale/config/config.yaml:/etc/headscale/config.yaml
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
ROOT_API_KEY: 'GENERATE_API_KEY' # Generate with: openssl rand -hex 16
COOKIE_SECRET: 'SAME_AS_CONFIG_YAML' # Same as in config.yaml
DISABLE_API_KEY_LOGIN: 'true'
networks:
- pangolin
# No Traefik labels needed - we're routing through main Traefik
networks:
pangolin:
external: true # Connect to existing Pangolin network
Generate a secure API key:
openssl rand -hex 16
Replace GENERATE_API_KEY
with the generated value, and make sure SAME_AS_CONFIG_YAML
matches the cookie secret in your Headplane config.yaml.
Part 6: DNS Configuration
Make sure you set up DNS records for:
heads.intranet.hhf.technology
- Points to your server IP*.ts.hhf.technology
- Wildcard record for Tailscale node hostnames
Part 7: Launch the Services
Now it’s time to start everything:
# Start Headscale and Headplane
docker compose -f docker-compose.headscale.yml up -d
# Restart Pangolin and Traefik to apply configuration changes
docker compose down
docker compose up -d
Part 8: Generate Headscale API Key
After everything is running, generate an API key for Headscale:
docker compose -f docker-compose.headscale.yml exec headscale headscale apikeys create
Save this API key for future use with API interactions. You have to place it in compose file of headscale and this will be used to login also.
Part 9: Debugging and Troubleshooting
Here are some useful commands for debugging your setup:
Check if Containers are Running
docker ps | grep -E 'headscale|headplane|traefik|pangolin|gerbil'
View Container Logs
# Traefik logs
docker compose logs -f traefik
# Headscale logs
docker compose -f docker-compose.headscale.yml logs -f headscale
# Headplane logs
docker compose -f docker-compose.headscale.yml logs -f headplane
Check Network Connectivity
# List networks
docker network ls
# Inspect the pangolin network
docker network inspect pangolin
# Make sure all containers are connected to the right network
docker inspect -f '{{.NetworkSettings.Networks}}' headscale
docker inspect -f '{{.NetworkSettings.Networks}}' headplane
Test Connectivity Between Containers
# Test if headplane can reach headscale
docker compose -f docker-compose.headscale.yml exec headplane wget -O- http://headscale:8080/health
# Test if Traefik can reach headscale
docker compose exec traefik wget -O- http://headscale:8080/health
Check DNS Resolution
# Test DNS resolution for your domains
dig heads.intranet.hhf.technology
dig test.ts.hhf.technology
Verify Certificate Issues
# Check Traefik's acme.json file
docker compose exec traefik cat /letsencrypt/acme.json
Part 10: Using Headscale with Tailscale Clients
Now that your Headscale server is running, you can start connecting clients:
Create Your First Namespace
docker compose -f docker-compose.headscale.yml exec headscale headscale namespaces create home
Generate a Pre-Auth Key
docker compose -f docker-compose.headscale.yml exec headscale headscale --user home preauthkeys create --expiration 24h
This will output a pre-auth key that you can use to connect clients.
Connect a Client
On a client machine, install the official Tailscale client and run:
tailscale up --login-server=https://heads.intranet.hhf.technology --authkey=YOUR_AUTH_KEY
Replace YOUR_AUTH_KEY
with the pre-auth key you generated.
Use the Headplane Web UI
Access the Headplane admin interface at:
https://heads.intranet.hhf.technology/admin
From here, you can:
- Manage nodes and namespaces
- View connection status
- Add or remove routes
- Generate pre-auth keys
- Configure ACLs
Common Issues and Solutions
404 Error on Root Domain
If you get a 404 error on the root domain but /admin
works, check your root router configuration:
headscale-root-rtr:
rule: "Host(`heads.intranet.hhf.technology`) && Path(`/`)"
service: noop@internal # Critical: use noop@internal, not a backend service
middlewares:
- headscale-redirect
priority: 100
Certificate Issues with Wildcard Domains
For the Tailscale subdomain wildcard certificate, you may need to use DNS challenge instead of HTTP challenge. Update your Traefik configuration accordingly.
Containers Can’t Communicate
If containers can’t communicate with each other, make sure they’re on the same Docker network:
# List networks each container is connected to
docker inspect -f '{{range $key, $value := .NetworkSettings.Networks}}{{$key}} {{end}}' headscale
docker inspect -f '{{range $key, $value := .NetworkSettings.Networks}}{{$key}} {{end}}' traefik
If needed, connect a container to the network:
docker network connect pangolin headscale
Debugging Redirect Issues
To debug redirect issues, check the response headers:
curl -I https://heads.intranet.hhf.technology
The response should include a Location
header with the redirect URL.
Conclusion
You now have a complete Headscale + Headplane setup integrated with your existing Pangolin installation. This powerful combination gives you:
- An identity-aware reverse proxy with Pangolin
- A self-hosted Tailscale control server with Headscale
- A user-friendly UI for managing your Tailscale network with Headplane
- All services accessible through a single Traefik instance
This integrated setup provides a comprehensive solution for secure network access, identity management, and service exposure in your self-hosted environment.