juanfont/headscale · critical

getting DERPMap: %w

Error message

getting DERPMap: %w

What it means

derp.GetDERPMap(h.cfg.DERP) failed while assembling the initial DERP map at startup (hscontrol/derp/derp.go:94). The function merges an inline DERPMap config, every entry in derp.urls (fetched over HTTP), and every entry in derp.paths (loaded from disk); any single source failing aborts startup. Typical wrapped causes are an unreachable/invalid URL, an HTTP fetch error, or a path that is missing/unparseable.

Source

Thrown at hscontrol/app.go:560

		Msg("Clients with a lower minimum version will be rejected")

	h.mapBatcher = mapper.NewBatcherAndMapper(h.cfg, h.state)

	h.mapBatcher.Start()
	defer h.mapBatcher.Close()

	if h.cfg.DERP.ServerEnabled {
		// When embedded DERP is enabled we always need a STUN server
		if h.cfg.DERP.STUNAddr == "" {
			return errSTUNAddressNotSet
		}

		go h.DERPServer.ServeSTUN()
	}

	derpMap, err := derp.GetDERPMap(h.cfg.DERP)
	if err != nil {
		return fmt.Errorf("getting DERPMap: %w", err)
	}

	if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
		region, _ := h.DERPServer.GenerateRegion()
		derpMap.Regions[region.RegionID] = &region
	}

	if len(derpMap.Regions) == 0 {
		return errEmptyInitialDERPMap
	}

	h.state.SetDERPMap(derpMap)

	// Start ephemeral node garbage collector and schedule all nodes
	// that are already in the database and ephemeral. If they are still
	// around between restarts, they will reconnect and the GC will
	// be cancelled.
	go h.ephemeralGC.Start()

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check the wrapped error — it names whether the failure came from a URL or a path
  2. For air-gapped installs, remove default derp.urls and provide a local file via derp.paths instead
  3. curl -fsSL <url> each configured URL from the headscale host to verify reachability and JSON validity
  4. Validate path-based maps: jq <path> must parse and match the DERPMap schema (Regions with Nodes)

Example fix

# before (air-gapped host, default urls unreachable)
derp:
  urls:
    - https://controlplane.tailscale.com/derpmap/default

# after
derp:
  urls: []
  paths:
    - /etc/headscale/derpmap.json
Defensive patterns

Strategy: validation

Validate before calling

for _, u := range cfg.DERP.URLs {
    resp, err := http.Head(u) // or GET with timeout
    if err != nil || resp.StatusCode != 200 {
        return fmt.Errorf("DERP map URL unreachable: %s", u)
    }
}
for _, p := range cfg.DERP.Paths {
    if _, err := os.Stat(p); err != nil {
        return fmt.Errorf("DERP map path missing: %s", p)
    }
}

Try / catch

if _, err := derp.GetDERPMap(h.cfg.DERP); err != nil {
    // single-source failure aborts startup; check wrapped error to see if it was
    // a URL (network) or path (filesystem) source and fix that one source
    return fmt.Errorf("getting DERPMap: %w", err)
}

Prevention

When it happens

Trigger: A derp.urls entry that is not a valid URL or returns non-200/unreachable (air-gapped host fetching the default Tailscale DERP map), TLS failures, a derp.paths file that does not exist or is invalid JSON/YAML, or a parse failure of the downloaded map.

Common situations: Offline/air-gapped deployments keeping the default derp.urls; a typo'd custom DERP map URL; DERP map file with schema drift after upgrading headscale; firewall blocking the control host's egress; self-hosted map served with a bad certificate.

Related errors


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