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
- Re-enable the embedded DERP server (derp.server.enabled: true with stun_addr set) so its region populates the map
- 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)
- 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
- If disabling the embedded DERP server, always add derp.urls in the same change
- Monitor control_logs for 'getting DERPMap' errors when derp.urls fetches fail
- Air-gapped deployments: serve the DERP map from a local file:// URL
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
- STUN address not set
- database type not supported
- unsupported policy mode
- failed to allocate IP
- path cannot be empty
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/5db8893caabfc6e5.
Report an issue: GitHub.