Overview
The Headscale stack uses Docker Compose to orchestrate multiple services. This page provides a complete reference for all services, configuration options, volumes, and networks.
The base docker-compose.yml is production-ready with SSL/TLS support. For local development, use docker-compose.override.yml to override production settings.
Services
Headscale
The main Headscale control server that coordinates the VPN mesh network.
headscale :
image : headscale/headscale:v0.27.0
container_name : headscale
restart : unless-stopped
command : serve
environment :
- TZ=${TZ:-UTC}
volumes :
- ./config:/etc/headscale
- ./data:/var/lib/headscale
networks :
- headscale-network
ports :
- 127.0.0.1:9090:9090 # Metrics endpoint (localhost only)
depends_on :
postgres :
condition : service_healthy
healthcheck :
test : [ CMD , headscale , health ]
interval : 30s
timeout : 10s
retries : 3
start_period : 10s
Key Features:
Version : Pinned to v0.27.0 for stability
Command : Runs in server mode (serve)
Health Check : Built-in health check command
Metrics : Prometheus metrics on port 9090 (localhost only for security)
Dependencies : Waits for PostgreSQL to be healthy before starting
Environment Variables:
Variable Default Description TZ UTC Timezone for logs and timestamps
Volumes:
Host Path Container Path Purpose ./config /etc/headscale Configuration files (config.yaml, ACL policies) ./data /var/lib/headscale Headscale data (keys, state)
nginx
Reverse proxy with SSL/TLS termination, rate limiting, and security headers.
nginx :
image : nginx:alpine
container_name : nginx
restart : unless-stopped
ports :
- 80:80 # HTTP (redirects to HTTPS)
- 443:443 # HTTPS
volumes :
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./logs/nginx:/var/log/nginx
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
networks :
- headscale-network
depends_on :
headscale :
condition : service_healthy
healthcheck :
test : [ CMD , wget , --quiet , --tries=1 , --spider , http://localhost:8080/health ]
interval : 30s
timeout : 5s
retries : 3
start_period : 10s
environment :
- HEADSCALE_DOMAIN=${HEADSCALE_DOMAIN:-headscale.example.com}
Key Features:
SSL/TLS : Production-ready with Let’s Encrypt certificates
HTTP/2 : Enabled for better performance
Rate Limiting : Protects against DDoS attacks
Security Headers : HSTS, CSP, X-Frame-Options, etc.
WebSocket Support : Required for Tailscale protocol
Ports:
Port Protocol Purpose 80 HTTP ACME challenges and redirect to HTTPS 443 HTTPS Main application traffic
Development Override:
docker-compose.override.yml
nginx :
ports :
- 8000:8080 # HTTP only, no SSL
volumes :
- ./nginx.dev.conf:/etc/nginx/nginx.conf:ro
- ./logs/nginx:/var/log/nginx
Headplane
Web-based management interface for Headscale.
headplane :
image : ghcr.io/tale/headplane:latest
container_name : headplane
restart : unless-stopped
ports :
- 3001:3000
volumes :
- ./headplane:/etc/headplane
- /var/run/docker.sock:/var/run/docker.sock:ro
networks :
- headscale-network
depends_on :
- headscale
environment :
- TZ=${TZ:-UTC}
- HEADPLANE_API_KEY=${HEADPLANE_API_KEY}
- HEADPLANE_COOKIE_SECRET=${HEADPLANE_COOKIE_SECRET}
healthcheck :
test : [ CMD , wget , --quiet , --tries=1 , --spider , http://localhost:3000/health ]
interval : 30s
timeout : 5s
retries : 3
start_period : 15s
Key Features:
Web UI : Modern web interface for managing Headscale
API Integration : Uses Headscale API for all operations
Docker Integration : Can manage containers (read-only socket)
Environment Variables:
Variable Required Description HEADPLANE_API_KEY Yes API key from Headscale HEADPLANE_COOKIE_SECRET Yes Random secret for session cookies (24 characters) TZ No Timezone (default: UTC)
Access Points:
Direct: http://localhost:3001/admin/
Via nginx: https://your-domain.com/admin
The Headplane URL requires a trailing slash: /admin/
PostgreSQL
Database backend for Headscale.
postgres :
image : postgres:18-alpine
container_name : headscale-db
restart : unless-stopped
environment :
POSTGRES_DB : ${POSTGRES_DB:-headscale}
POSTGRES_USER : ${POSTGRES_USER:-headscale}
POSTGRES_PASSWORD : ${POSTGRES_PASSWORD:-changeme}
volumes :
- postgres-data:/var/lib/postgresql/data
networks :
- headscale-network
healthcheck :
test : [ CMD-SHELL , "pg_isready -U $${POSTGRES_USER:-headscale}" ]
interval : 10s
timeout : 5s
retries : 5
Key Features:
Version : PostgreSQL 18 (Alpine for smaller image)
Health Check : Verifies database is ready before starting Headscale
Persistence : Named volume for data persistence
Environment Variables:
Variable Default Description POSTGRES_DB headscale Database name POSTGRES_USER headscale Database user POSTGRES_PASSWORD changeme Database password (MUST change in production)
The POSTGRES_PASSWORD in .env must match the database.postgres.password in config/config.yaml.
Database Management:
# Connect to database
docker exec -it headscale-db psql -U headscale
# Backup database
docker exec headscale-db pg_dump -U headscale headscale > backup.sql
# Restore database
cat backup.sql | docker exec -i headscale-db psql -U headscale
# Check database size
docker exec -it headscale-db psql -U headscale -c "\l+"
Certbot
Automated SSL/TLS certificate management with Let’s Encrypt.
certbot :
image : certbot/certbot:latest
container_name : certbot
restart : unless-stopped
volumes :
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint : "/bin/sh -c 'trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done;'"
depends_on :
- nginx
Key Features:
Auto-Renewal : Checks for renewal every 12 hours
Silent Operation : Runs quietly unless there are errors
Let’s Encrypt : Free SSL certificates with automatic renewal
Certificate Management:
# Obtain new certificate
docker compose run --rm certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
--email [email protected] \
--agree-tos \
--no-eff-email \
-d headscale.example.com
# Manual renewal
docker compose run --rm certbot renew
# Force renewal (testing)
docker compose run --rm certbot renew --force-renewal
# List certificates
docker compose run --rm certbot certificates
Development Override:
docker-compose.override.yml
certbot :
entrypoint : /bin/sh -c "echo 'Certbot disabled in development mode'; sleep infinity"
restart : "no"
Networks
headscale-network
Bridge network for internal service communication.
networks :
headscale-network :
driver : bridge
Key Features:
Isolation : Services communicate internally without exposing ports to host
DNS : Docker provides automatic DNS resolution between services
Bridge Mode : Standard Docker networking
Service Communication:
From To Address nginx headscale http://headscale:8080 nginx headplane http://headplane:3000 headscale postgres postgres:5432
Volumes
Named Volumes
volumes :
postgres-data :
driver : local
postgres-data
Purpose : PostgreSQL database files
Location : Docker managed volume
Persistence : Data survives container restarts and recreation
Backup : Use pg_dump to backup data
# Check volume location
docker volume inspect headscale-tailscale-docker_postgres-data
# Backup volume
docker exec headscale-db pg_dump -U headscale headscale > backup.sql
# Remove volume (deletes all data!)
docker compose down -v
Bind Mounts
Host Path Container Path Purpose ./config headscale /etc/headscale Configuration files ./data headscale /var/lib/headscale Headscale state ./nginx.conf nginx /etc/nginx/nginx.conf nginx configuration ./logs/nginx nginx /var/log/nginx nginx logs ./certbot/conf nginx, certbot /etc/letsencrypt SSL certificates ./certbot/www nginx, certbot /var/www/certbot ACME challenge ./headplane headplane /etc/headplane Headplane config
Environment Variables
All environment variables are defined in .env:
# Domain Configuration
HEADSCALE_DOMAIN = headscale.example.com
# PostgreSQL Configuration
POSTGRES_DB = headscale
POSTGRES_USER = headscale
POSTGRES_PASSWORD = changeme_to_secure_password
# Timezone
TZ = UTC
# Headplane Configuration
HEADPLANE_API_KEY = your_headscale_api_key_here
HEADPLANE_COOKIE_SECRET = your_random_cookie_secret_here
Variable Reference:
Variable Used By Required Default HEADSCALE_DOMAIN nginx Yes headscale.example.com POSTGRES_DB postgres, headscale No headscale POSTGRES_USER postgres, headscale No headscale POSTGRES_PASSWORD postgres, headscale Yes changeme TZ headscale, headplane No UTC HEADPLANE_API_KEY headplane Yes - HEADPLANE_COOKIE_SECRET headplane Yes -
Never commit .env to version control. Add it to .gitignore.
Port Mapping
Production Mode
Service Host Port Container Port Protocol Purpose nginx 80 80 HTTP ACME challenges, redirect to HTTPS nginx 443 443 HTTPS Main application traffic headplane 3001 3000 HTTP Direct access to Headplane UI headscale 127.0.0.1:9090 9090 HTTP Prometheus metrics (localhost only)
Development Mode (with override)
Service Host Port Container Port Protocol Purpose nginx 8000 8080 HTTP Development HTTP access headplane 3001 3000 HTTP Direct access to Headplane UI headscale 127.0.0.1:8080 8080 HTTP Development metrics/debugging
Health Checks
All services include health checks for reliable startup and monitoring.
Headscale
healthcheck :
test : [ CMD , headscale , health ]
interval : 30s
timeout : 10s
retries : 3
start_period : 10s
nginx
healthcheck :
test : [ CMD , wget , --quiet , --tries=1 , --spider , http://localhost:8080/health ]
interval : 30s
timeout : 5s
retries : 3
start_period : 10s
PostgreSQL
healthcheck :
test : [ CMD-SHELL , "pg_isready -U $${POSTGRES_USER:-headscale}" ]
interval : 10s
timeout : 5s
retries : 5
Headplane
healthcheck :
test : [ CMD , wget , --quiet , --tries=1 , --spider , http://localhost:3000/health ]
interval : 30s
timeout : 5s
retries : 3
start_period : 15s
Check Health Status:
# View health status
docker compose ps
# Check specific service
docker inspect headscale --format= '{{.State.Health.Status}}'
Service Dependencies
Startup Order:
postgres starts first and waits until healthy
headscale starts after postgres is healthy
nginx starts after headscale is healthy
headplane starts after headscale is running
certbot starts after nginx is running
Development Override
The docker-compose.override.yml file overrides production settings for local development:
docker-compose.override.example.yml
version : '3.8'
services :
headscale :
ports :
- 127.0.0.1:8080:8080
environment :
- LOG_LEVEL=${LOG_LEVEL:-info}
nginx :
ports :
- 8000:8080
volumes :
- ./nginx.dev.conf:/etc/nginx/nginx.conf:ro
- ./logs/nginx:/var/log/nginx
certbot :
entrypoint : /bin/sh -c "echo 'Certbot disabled in development mode'; sleep infinity"
restart : "no"
Key Changes:
Uses nginx.dev.conf (HTTP only, no SSL)
Exposes HTTP on port 8000
Disables certbot
Enables debug logging
Usage:
# Create override file
cp docker-compose.override.example.yml docker-compose.override.yml
# Start with overrides applied automatically
docker compose up -d
# Remove override to use production config
rm docker-compose.override.yml
Common Commands
Lifecycle
Logs
Status
Updates
# Start all services
docker compose up -d
# Stop all services
docker compose down
# Restart specific service
docker compose restart nginx
# Recreate service
docker compose up -d --force-recreate headscale
# View all logs
docker compose logs -f
# View specific service
docker compose logs -f headscale
# Last 50 lines
docker compose logs --tail 50 nginx
# Since timestamp
docker compose logs --since 30m
# List all services
docker compose ps
# List with health status
docker compose ps --format json
# Resource usage
docker stats
# Inspect service
docker compose config
# Pull latest images
docker compose pull
# Rebuild and restart
docker compose up -d --build
# Remove old images
docker image prune -a
Troubleshooting
Service Won’t Start
# Check service status
docker compose ps
# View logs
docker compose logs < service-nam e >
# Check configuration
docker compose config
# Validate compose file
docker compose config --quiet
Port Conflicts
# Check what's using a port
sudo lsof -i :80
sudo lsof -i :443
# Change ports in docker-compose.override.yml
Volume Issues
# List volumes
docker volume ls
# Inspect volume
docker volume inspect < volume-nam e >
# Remove unused volumes
docker volume prune
# Backup volume
docker run --rm -v < volum e > :/data -v $( pwd ) :/backup alpine tar czf /backup/backup.tar.gz /data
Network Issues
# List networks
docker network ls
# Inspect network
docker network inspect headscale-tailscale-docker_headscale-network
# Recreate network
docker compose down
docker compose up -d
Next Steps
Local Development Set up local development environment
Production Deployment Deploy to production with SSL/TLS