HOW-TO

Deploying your own coordination plane, start to finish. Headscale's config schema has broken between versions before — check yours against the official docs if a step here doesn't match.

  1. 01 Install Docker

    The only real dependency. Docker Desktop on Mac/Windows, or just the Docker Engine on Linux.

    brew install --cask docker
  2. 02 Write docker-compose.yml

    Defines the headscale container. command: serve, not headscale serve — the binary name is already the entrypoint.

    services:
      headscale:
        image: headscale/headscale:latest
        container_name: headscale
        command: serve
        volumes:
          - ./config:/etc/headscale
          - ./data:/var/lib/headscale
        ports:
          - "8080:8080"
          - "9090:9090"
        restart: unless-stopped
  3. 03 Write config/config.yaml

    Current schema as of headscale v0.29.x. Needs at least one DERP entry or it refuses to boot — see Field Notes #2.

    server_url: http://YOUR_LAN_IP:8080
    listen_addr: 0.0.0.0:8080
    metrics_listen_addr: 0.0.0.0:9090
    
    noise:
      private_key_path: /var/lib/headscale/noise_private.key
    
    prefixes:
      v4: 100.64.0.0/10
      v6: fd7a:115c:a1e0::/48
    
    derp:
      server:
        enabled: false
      urls:
        - https://controlplane.tailscale.com/derpmap/default
    
    database:
      type: sqlite
      sqlite:
        path: /var/lib/headscale/db.sqlite
    
    policy:
      mode: file
      path: /etc/headscale/acl.json
    
    dns:
      magic_dns: true
      base_domain: yourdomain.internal
      nameservers:
        global:
          - 1.1.1.1
  4. 04 Write config/acl.json

    Closed-by-default — only devices under the same user can reach each other. Not the wide-open accept-all default.

    {
      "groups": { "group:home": ["youruser@"] },
      "acls": [
        { "action": "accept", "src": ["group:home"], "dst": ["group:home:*"] }
      ]
    }
  5. 05 Start the stack

    Confirm it's actually staying up, not crash-looping — check twice, a few seconds apart.

    docker compose up -d
    docker ps
  6. 06 Create a user and a pre-auth key

    Use the numeric user ID, not the name, for preauthkeys — see Field Notes #4.

    docker exec -it headscale headscale users create myuser
    docker exec -it headscale headscale preauthkeys create --user 1 --reusable --expiration 24h
  7. 07 Connect your first device

    If Tailscale's already managing this device for another tailnet, use login (adds a profile) rather than up --reset (replaces the active one).

    sudo tailscale login --login-server=http://YOUR_LAN_IP:8080 --authkey=YOUR_KEY