Skip to main content

Overview

The local development environment uses a simplified HTTP-only setup without SSL/TLS certificates. This configuration is perfect for testing and development on your local machine.
The stack uses a production-first approach. The base docker-compose.yml is production-ready, and local development uses an override file to simplify the configuration.

Prerequisites

  • Docker and Docker Compose installed
  • At least 2GB of available RAM
  • 5GB of free disk space

Quick Start

1

Clone the Repository

Clone the Headscale Docker stack to your local machine:
git clone <your-repo>
cd headscale-tailscale-docker
2

Create Development Override

Copy the example override file to enable local development mode:
cp docker-compose.override.example.yml docker-compose.override.yml
The docker-compose.override.yml file is gitignored and won’t be committed to version control.
This override file makes the following changes:
  • Uses nginx.dev.conf instead of nginx.conf (no SSL)
  • Exposes HTTP on port 8000 instead of 80/443
  • Disables the certbot service (not needed for dev)
3

Configure Environment Variables

Copy the environment template and configure for local development:
cp .env.example .env
nano .env
Set these values for development:
.env
# Use localhost for development
HEADSCALE_DOMAIN=localhost

# PostgreSQL credentials
POSTGRES_DB=headscale
POSTGRES_USER=headscale
POSTGRES_PASSWORD=your_secure_password

# Timezone
TZ=UTC

# Headplane configuration (generate these after starting the stack)
HEADPLANE_API_KEY=your_api_key_here
HEADPLANE_COOKIE_SECRET=your_cookie_secret_here
Even in development, use a secure password for PostgreSQL. Don’t use “changeme” or simple passwords.
4

Start the Stack

Start all services using Docker Compose:
docker compose up -d
Check the status of all services:
docker compose ps
View logs to ensure everything is running:
docker compose logs -f
5

Verify Health

Test the Headscale health endpoint:
curl http://localhost:8000/health
Expected response:
{"status":"pass"}

Access Points

Once the stack is running, you can access the following services:
ServiceURLDescription
Headscale APIhttp://localhost:8000Main API endpoint
Headplane UIhttp://localhost:3001/admin/Web management interface
Health Checkhttp://localhost:8000/healthService health status
Metricshttp://localhost:9090/metricsPrometheus metrics (localhost only)
The Headplane UI requires a trailing slash: http://localhost:3001/admin/

Initial Configuration

Generate API Key

Create an API key for Headplane and CLI access:
docker exec headscale headscale apikeys create --expiration 999d
Copy the generated key and add it to your .env file:
HEADPLANE_API_KEY=<your-generated-key>
Create a random cookie secret for Headplane sessions:
openssl rand -base64 24
Add it to your .env file:
HEADPLANE_COOKIE_SECRET=<your-generated-secret>

Restart Headplane

Restart Headplane to apply the new configuration:
docker compose restart headplane

Development Workflow

Managing Services

# Start all services
docker compose up -d

# Stop all services
docker compose down

# Restart a specific service
docker compose restart nginx
docker compose restart headscale

Create Your First User

Create a user for managing devices:
# Using docker exec
docker exec headscale headscale users create myuser

# Or using the helper script
./scripts/headscale.sh users create myuser
List all users:
./scripts/headscale.sh users list

Generate Pre-Auth Keys

Create a reusable pre-auth key for connecting devices:
# Using docker exec
docker exec headscale headscale preauthkeys create --user myuser --reusable --expiration 24h

# Or using the helper script
./scripts/headscale.sh keys create myuser --reusable --expiration 24h
Save the generated key - you’ll need it to connect devices to your Headscale server.

Connecting a Device

Connect a device to your local Headscale server:
sudo tailscale up --login-server http://localhost:8000 --authkey YOUR_KEY --accept-routes

Configuration Files

Docker Compose Override

The docker-compose.override.yml file contains development-specific settings:
docker-compose.override.yml
version: '3.8'

services:
  headscale:
    # Expose metrics port for debugging
    ports:
      - 127.0.0.1:8080:8080

    environment:
      - LOG_LEVEL=${LOG_LEVEL:-info}

  nginx:
    # Development HTTP port (no SSL)
    ports:
      - 8000:8080
    volumes:
      # Use development config (simple HTTP, no SSL)
      - ./nginx.dev.conf:/etc/nginx/nginx.conf:ro
      - ./logs/nginx:/var/log/nginx

  # Disable certbot for local development
  certbot:
    entrypoint: /bin/sh -c "echo 'Certbot disabled in development mode'; sleep infinity"
    restart: "no"

nginx Development Config

Key differences in nginx.dev.conf:
  • Listens on port 8080 (HTTP only)
  • No SSL/TLS configuration
  • Simplified security headers
  • No rate limiting for easier testing
  • Direct proxy to Headscale on port 8080

Troubleshooting

Port Already in Use

If port 8000 is already in use:
# Check what's using the port
sudo lsof -i :8000

# Stop the conflicting service or change the port in docker-compose.override.yml

nginx Won’t Start

Test the nginx configuration:
./scripts/nginx.sh test
Check nginx logs:
./scripts/nginx.sh error-logs 50

Headscale Connection Issues

Check if Headscale is running:
docker compose ps headscale
View Headscale logs:
docker compose logs -f headscale
Verify database connection:
docker exec -it headscale-db psql -U headscale -c "SELECT 1;"

Database Password Mismatch

The PostgreSQL password must match in both .env and config/config.yaml.
If you see database connection errors, verify the passwords match:
# Check .env
grep POSTGRES_PASSWORD .env

# Check config.yaml
grep password config/config.yaml

Headplane Won’t Load

Common issues:
  1. Missing API Key: Generate an API key and add it to .env
  2. Wrong URL: Access http://localhost:3001/admin/ with trailing slash
  3. Cookie Secret Length: Must be exactly 32 characters
Check Headplane logs:
docker logs headplane --tail 50

Cleanup

Stop and Remove All Services

# Stop services but keep data
docker compose down

# Stop services and remove volumes (deletes all data)
docker compose down -v

Reset to Clean State

# Stop everything
docker compose down -v

# Remove generated files
rm -rf data/* logs/*

# Start fresh
docker compose up -d

Next Steps

Production Deployment

Deploy to production with SSL/TLS

Docker Compose Reference

Learn about all Docker Compose services

Build docs developers (and LLMs) love