# Headscale + Headplane - Standalone Docker Compose Stack # Headscale: self-hosted Tailscale control server # Headplane: third-party Headscale web UI # # Usage: # cp headscale.env.example headscale.env # edit headscale.env # docker compose --env-file headscale.env -f headscale-compose.yml up -d # # Reverse proxy expectation: # - Proxy the public Headscale hostname to HEADSCALE_HTTP_PORT on this Docker host. # - Keep Headplane LAN/tailnet-only initially unless explicitly exposed later. name: headscale services: headscale: container_name: headscale image: headscale/headscale:${HEADSCALE_VERSION} restart: unless-stopped command: serve labels: # Required by Headplane's Docker integration so it can locate/restart Headscale. me.tale.headplane.target: headscale ports: # Headscale HTTP API / coordination endpoint. Intended for Pangolin reverse proxy. - "${HEADSCALE_HTTP_PORT}:8080" # Metrics/debug listener is bound to localhost in config by default; exposed only on Docker network. # gRPC is intentionally not published to the host for the initial setup. volumes: - ${HEADSCALE_CONFIG_DIR}/config.yaml:/etc/headscale/config.yaml:rw - ${HEADSCALE_CONFIG_DIR}/dns_records.json:/etc/headscale/dns_records.json:rw - ${HEADSCALE_DATA_DIR}:/var/lib/headscale:rw - /etc/localtime:/etc/localtime:ro healthcheck: test: ["CMD", "headscale", "version"] interval: 30s timeout: 10s retries: 5 start_period: 30s networks: - headscale-net headplane: container_name: headplane image: ghcr.io/tale/headplane:${HEADPLANE_VERSION} restart: unless-stopped depends_on: headscale: condition: service_started ports: # LAN/tailnet-only admin UI by default. Do not expose publicly until auth/proxy policy is reviewed. - "${HEADPLANE_HTTP_PORT}:3000" volumes: - ${HEADPLANE_CONFIG_DIR}/config.yaml:/etc/headplane/config.yaml:ro - ${HEADPLANE_SECRETS_DIR}:/run/secrets/headplane:ro - ${HEADPLANE_DATA_DIR}:/var/lib/headplane:rw # Shared Headscale config access enables Headplane network/DNS/config management. - ${HEADSCALE_CONFIG_DIR}/config.yaml:/etc/headscale/config.yaml:rw - ${HEADSCALE_CONFIG_DIR}/dns_records.json:/etc/headscale/dns_records.json:rw # Docker socket is read-only; Headplane uses it to find/restart Headscale after config changes. - /var/run/docker.sock:/var/run/docker.sock:ro - /etc/localtime:/etc/localtime:ro networks: - headscale-net networks: headscale-net: name: headscale-net driver: bridge