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] = ®ion
}
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
- Check the wrapped error — it names whether the failure came from a URL or a path
- For air-gapped installs, remove default derp.urls and provide a local file via derp.paths instead
- curl -fsSL <url> each configured URL from the headscale host to verify reachability and JSON validity
- 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
- On air-gapped hosts, replace derp.urls with a local derp.paths file
- Serve custom DERP maps from highly available endpoints with valid TLS
- Validate every configured URL/path from the headscale host before restart
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
- building trusted_proxies middleware: %w
- DERP server private key and noise private key are the same:
- binding to TCP address: %w
- STUN address not set
- initial DERPMap is empty, Headscale requires at least one en
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/a7397a7dfb64f241.
Report an issue: GitHub.