Skip to main content
The Headscale + Tailscale Docker Stack is a production-ready containerized deployment that provides a complete self-hosted Tailscale control plane.

System architecture

┌─────────────────────────────────────────────────────────────┐
│                    Client Devices                           │
│              (Tailscale Clients)                            │
└───────────────────────┬─────────────────────────────────────┘

                        │ HTTP/HTTPS


┌─────────────────────────────────────────────────────────────┐
│                 nginx Reverse Proxy                         │
│                                                             │
│  Development: HTTP on port 8000                             │
│  Production:  HTTPS on port 443 (HTTP redirect on 80)      │
│                                                             │
│  Features:                                                  │
│  • WebSocket support                                        │
│  • Rate limiting                                            │
│  • SSL/TLS termination                                      │
│  • Security headers                                         │
└─────────┬───────────────────────────┬───────────────────────┘
          │                           │
          │                           │
┌─────────▼──────────┐    ┌──────────▼────────┐
│   Headscale        │    │   Headplane       │
│   v0.27.0          │    │   (Web GUI)       │
│   Port: 8080       │    │   Port: 3000      │
│                    │    │                   │
│   Endpoints:       │    │   Path: /admin/   │
│   • /              │    └───────────────────┘
│   • /api/*         │
│   • /health        │
│   • /metrics       │
└─────────┬──────────┘


┌─────────▼──────────┐    ┌──────────────────┐
│   PostgreSQL 18    │    │   Certbot        │
│   Port: 5432       │    │   (SSL Certs)    │
│                    │    │                  │
│   Database:        │    │   Auto-renewal   │
│   headscale        │    │   every 12h      │
└────────────────────┘    └──────────────────┘

Component descriptions

Headscale server

Image: headscale/headscale:v0.27.0 The core control plane server that:
  • Manages device registration and authentication
  • Coordinates the mesh VPN network topology
  • Handles node discovery and NAT traversal via DERP
  • Stores network state in PostgreSQL
  • Provides REST API for management operations
  • Serves health and Prometheus metrics endpoints
Internal ports:
  • 8080 - HTTP API and control plane
  • 9090 - Metrics (exposed to localhost only)
  • 50443 - gRPC API

nginx reverse proxy

Image: nginx:alpine Provides:
  • SSL/TLS termination with Let’s Encrypt certificates
  • HTTP to HTTPS redirection (production)
  • Request routing to backend services
  • Rate limiting protection
  • Security headers (HSTS, CSP, X-Frame-Options)
  • WebSocket connection upgrades
  • Health check endpoint proxying
  • Access and error logging
External ports:
  • Development: 8000 (HTTP only)
  • Production: 80 (HTTP redirect), 443 (HTTPS)

PostgreSQL database

Image: postgres:18-alpine Persistent data storage for:
  • User accounts
  • Node registrations
  • Pre-authentication keys
  • Route advertisements
  • ACL policies (when using database mode)
  • Audit logs
Features:
  • Health checks via pg_isready
  • Data persistence with Docker volume
  • Connection pooling (max 10 connections)
  • Automatic dependency management (headscale waits for healthy DB)

Headplane web GUI

Image: ghcr.io/tale/headplane:latest Web-based management interface providing:
  • User management (create, list, delete)
  • Node overview and management
  • Pre-auth key generation
  • Route approval
  • ACL policy editor
  • Real-time status monitoring
Access: http://localhost:3001/admin/

Certbot

Image: certbot/certbot:latest Automated SSL/TLS certificate management:
  • Obtains Let’s Encrypt certificates
  • Renews certificates automatically every 12 hours
  • Uses HTTP-01 ACME challenge
  • Shares certificate directory with nginx

Network architecture

Docker network

All services communicate via the headscale-network bridge network:
networks:
  headscale-network:
    driver: bridge
Internal DNS: Services reference each other by container name:
  • headscale:8080 - Headscale API
  • postgres:5432 - Database connection
  • headplane:3000 - Web GUI

Service dependencies

Dependency chain ensures proper startup order:
postgres (healthy)

headscale (healthy)

nginx ← headplane

certbot
Health checks ensure dependent services don’t start until their dependencies are fully operational.

Data persistence

Volumes

PostgreSQL data:
postgres-data:/var/lib/postgresql/data
Named Docker volume for database files. Headscale configuration:
./config:/etc/headscale
Bind mount for config.yaml and policy.json. Headscale data:
./data:/var/lib/headscale
Bind mount for encryption keys and SQLite cache. SSL certificates:
./certbot/conf:/etc/letsencrypt
./certbot/www:/var/www/certbot
Bind mounts for Let’s Encrypt certificates and ACME challenge files. nginx logs:
./logs/nginx:/var/log/nginx
Bind mount for access and error logs.

Request flow

Client connection request

  1. Client initiates connection
    tailscale up --login-server https://yourdomain.com --authkey <key>
    
  2. nginx receives HTTPS request on port 443
    • Terminates SSL/TLS
    • Applies rate limiting (production)
    • Adds security headers
  3. nginx routes to Headscale at headscale:8080
    • Proxies / and /api/* endpoints
    • Enables WebSocket upgrades
    • Sets proxy headers (X-Real-IP, X-Forwarded-For)
  4. Headscale processes request
    • Validates pre-auth key
    • Registers node in PostgreSQL
    • Returns network configuration
  5. Client establishes mesh connectivity
    • Direct peer-to-peer when possible
    • Via DERP relay when NAT traversal fails

Health check flow

curl http://localhost:8000/health
  1. nginx receives request on /health
  2. Proxies to headscale:8080/health (no logging)
  3. Headscale responds with {"status":"pass"}
  4. nginx returns response to client

Metrics collection flow

curl http://localhost:9090/metrics
Direct access to Headscale metrics endpoint (localhost only):
  • Prometheus-format metrics
  • Node counts, API request rates, database query timing
  • Not proxied through nginx for security

Development vs production

Development mode

Characteristics:
  • HTTP only (no SSL/TLS)
  • Single port: 8000
  • Uses nginx.dev.conf (via docker-compose override)
  • No rate limiting
  • Simplified configuration
  • Localhost access only
Enable:
cp docker-compose.override.example.yml docker-compose.override.yml
docker compose up -d

Production mode

Characteristics:
  • HTTPS with Let’s Encrypt
  • Dual ports: 80 (redirect), 443 (HTTPS)
  • Uses nginx.conf (full security)
  • 3-tier rate limiting
  • Security headers (HSTS, CSP, etc.)
  • Public internet access
Enable:
# Remove override file if present
rm docker-compose.override.yml
docker compose up -d
Never expose development mode to the public internet. Always use production configuration for external access.

Security architecture

Authentication layers

  1. API authentication: Headplane uses API keys to communicate with Headscale
  2. Pre-auth keys: Devices authenticate using time-limited, single-use or reusable keys
  3. ACL policies: Tag-based access control restricts traffic between nodes
  4. SSL/TLS: All external communication encrypted (production)

Network isolation

Services run in isolated bridge network:
  • No direct external access to PostgreSQL
  • Headscale API only accessible via nginx
  • Metrics endpoint restricted to localhost
  • Headplane accessible only through authenticated session

Data encryption

  • At rest: PostgreSQL data in Docker volume
  • In transit: TLS 1.2+ with modern ciphers (production)
  • Mesh traffic: WireGuard protocol with Noise key exchange

Monitoring and observability

Health checks

All services have health checks:
headscale:
  healthcheck:
    test: [CMD, headscale, health]
    interval: 30s

postgres:
  healthcheck:
    test: [CMD-SHELL, "pg_isready -U headscale"]
    interval: 10s

nginx:
  healthcheck:
    test: [CMD, wget, --spider, http://localhost:8080/health]
    interval: 30s

Logging

  • nginx: Access and error logs to ./logs/nginx/
  • Headscale: Stdout/stderr captured by Docker
  • PostgreSQL: Transaction logs in Docker volume

Metrics

Prometheus metrics on http://localhost:9090/metrics:
  • API request counts and latency
  • Database connection pool stats
  • Active node count
  • Route advertisement count

Networking

DERP servers, subnet routing, and exit nodes

File structure

Directory layout and configuration files

Docker Compose

Service definitions and orchestration

nginx configuration

Reverse proxy setup and SSL configuration

Build docs developers (and LLMs) love