juanfont/headscale · critical

initial DERPMap is empty, Headscale requires at least one en

Error message

initial DERPMap is empty, Headscale requires at least one entry

What it means

Returned from Headscale.Serve() (hscontrol/app.go:568-570) when the DERP map assembled from the config has zero regions. Headscale requires at least one DERP region because Tailscale clients need a relay for NAT traversal; an empty map means the tailnet would be non-functional. The map is built by derp.GetDERPMap(h.cfg.DERP) and, if the embedded server is enabled with automatically_add_embedded_derp_region, its region is added — otherwise the map depends entirely on derp.urls entries.

Source

Thrown at hscontrol/app.go:57

	"github.com/pkg/profile"
	"github.com/rs/zerolog/log"
	"github.com/sasha-s/go-deadlock"
	"golang.org/x/crypto/acme"
	"golang.org/x/crypto/acme/autocert"
	"golang.org/x/sync/errgroup"
	"tailscale.com/envknob"
	"tailscale.com/tailcfg"
	"tailscale.com/types/dnstype"
	"tailscale.com/types/key"
	"tailscale.com/util/dnsname"
)

var (
	errSTUNAddressNotSet                   = errors.New("STUN address not set")
	errUnsupportedLetsEncryptChallengeType = errors.New(
		"unknown value for Lets Encrypt challenge type",
	)
	errEmptyInitialDERPMap = errors.New(
		"initial DERPMap is empty, Headscale requires at least one entry",
	)
)

var (
	debugDeadlock        = envknob.Bool("HEADSCALE_DEBUG_DEADLOCK")
	debugDeadlockTimeout = envknob.RegisterDuration("HEADSCALE_DEBUG_DEADLOCK_TIMEOUT")
)

func init() {
	deadlock.Opts.Disable = !debugDeadlock
	if debugDeadlock {
		deadlock.Opts.DeadlockTimeout = debugDeadlockTimeout()
		deadlock.Opts.PrintAllCurrentGoroutines = true
	}
}

const (

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Re-enable the embedded DERP server (derp.server.enabled: true with stun_addr set) so its region populates the map
  2. Or configure derp.urls with at least one valid DERP map URL / local file path (e.g. a file:///etc/headscale/derpmap.yaml with one region)
  3. Verify network egress to the DERP map URL if using the default Tailscale URL, or host the map locally

Example fix

# before
derp:
  server:
    enabled: false
  urls: []

# after
derp:
  server:
    enabled: false
  urls:
    - file:///etc/headscale/derpmap.yaml
Defensive patterns

Strategy: validation

Validate before calling

# assert the effective DERP map will be non-empty before startup:
# either embedded DERP is enabled, or at least one derp.urls entry exists
server_enabled=$(yq '.derp.server.enabled' config.yaml)
urls_len=$(yq '.derp.urls | length' config.yaml)
[ "$server_enabled" = "true" ] || [ "$urls_len" -gt 0 ] || { echo 'no DERP regions configured'; exit 1; }

Try / catch

if err := h.Serve(); err != nil {
    if errors.Is(err, errEmptyInitialDERPMap) {
        log.Fatal().Msg("configure derp.urls or enable the embedded DERP server (with stun_addr)")
    }
    return err
}

Prevention

When it happens

Trigger: derp.paths/derp.urls empty or all fetches fail silently leaving zero regions, embedded DERP disabled (derp.server.enabled: false) and no derp.urls configured, or derp.urls pointing at a URL returning an empty/invalid DERPMap.

Common situations: Fully disabling the embedded DERP server while forgetting to list external derp.urls; a typo in the default https://controlplane.tailscale.com/derpmap/default URL; an air-gapped instance that cannot fetch the default DERP map and has no local file configured.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/5db8893caabfc6e5. Report an issue: GitHub.