Skip to main content

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.
docker-compose.yml
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:
VariableDefaultDescription
TZUTCTimezone for logs and timestamps
Volumes:
Host PathContainer PathPurpose
./config/etc/headscaleConfiguration files (config.yaml, ACL policies)
./data/var/lib/headscaleHeadscale data (keys, state)

nginx

Reverse proxy with SSL/TLS termination, rate limiting, and security headers.
docker-compose.yml
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:
PortProtocolPurpose
80HTTPACME challenges and redirect to HTTPS
443HTTPSMain 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.
docker-compose.yml
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:
VariableRequiredDescription
HEADPLANE_API_KEYYesAPI key from Headscale
HEADPLANE_COOKIE_SECRETYesRandom secret for session cookies (24 characters)
TZNoTimezone (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.
docker-compose.yml
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:
VariableDefaultDescription
POSTGRES_DBheadscaleDatabase name
POSTGRES_USERheadscaleDatabase user
POSTGRES_PASSWORDchangemeDatabase 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.
docker-compose.yml
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.
docker-compose.yml
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:
FromToAddress
nginxheadscalehttp://headscale:8080
nginxheadplanehttp://headplane:3000
headscalepostgrespostgres:5432

Volumes

Named Volumes

docker-compose.yml
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 PathContainerPathPurpose
./configheadscale/etc/headscaleConfiguration files
./dataheadscale/var/lib/headscaleHeadscale state
./nginx.confnginx/etc/nginx/nginx.confnginx configuration
./logs/nginxnginx/var/log/nginxnginx logs
./certbot/confnginx, certbot/etc/letsencryptSSL certificates
./certbot/wwwnginx, certbot/var/www/certbotACME challenge
./headplaneheadplane/etc/headplaneHeadplane config

Environment Variables

All environment variables are defined in .env:
.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:
VariableUsed ByRequiredDefault
HEADSCALE_DOMAINnginxYesheadscale.example.com
POSTGRES_DBpostgres, headscaleNoheadscale
POSTGRES_USERpostgres, headscaleNoheadscale
POSTGRES_PASSWORDpostgres, headscaleYeschangeme
TZheadscale, headplaneNoUTC
HEADPLANE_API_KEYheadplaneYes-
HEADPLANE_COOKIE_SECRETheadplaneYes-
Never commit .env to version control. Add it to .gitignore.

Port Mapping

Production Mode

ServiceHost PortContainer PortProtocolPurpose
nginx8080HTTPACME challenges, redirect to HTTPS
nginx443443HTTPSMain application traffic
headplane30013000HTTPDirect access to Headplane UI
headscale127.0.0.1:90909090HTTPPrometheus metrics (localhost only)

Development Mode (with override)

ServiceHost PortContainer PortProtocolPurpose
nginx80008080HTTPDevelopment HTTP access
headplane30013000HTTPDirect access to Headplane UI
headscale127.0.0.1:80808080HTTPDevelopment 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:
  1. postgres starts first and waits until healthy
  2. headscale starts after postgres is healthy
  3. nginx starts after headscale is healthy
  4. headplane starts after headscale is running
  5. 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

# 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

Troubleshooting

Service Won’t Start

# Check service status
docker compose ps

# View logs
docker compose logs <service-name>

# 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-name>

# Remove unused volumes
docker volume prune

# Backup volume
docker run --rm -v <volume>:/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

Build docs developers (and LLMs) love