juanfont/headscale · critical
STUN address not set
Error message
STUN address not set
What it means
Headscale returns this sentinel from Headscale.Serve() (hscontrol/app.go:552) when the embedded DERP server is enabled (derp.server.enabled) but derp.server.stun_addr is empty. The embedded DERP relay always needs a STUN server, so the config combination is rejected at startup. Nothing is started; the process fails fast before serving.
Source
Thrown at hscontrol/app.go:53
"github.com/juanfont/headscale/hscontrol/state"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/types/change"
"github.com/juanfont/headscale/hscontrol/util"
"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 = trueView on GitHub (pinned to 565fd254d0)
Solutions
- Set derp.server.stun_addr in config.yaml (e.g. stun_addr: 0.0.0.0:3478) and ensure the UDP port is open on the host firewall
- If you do not want an embedded DERP/STUN server, set derp.server.enabled: false instead and rely on derp.urls / external regions
- Validate config with `headscale configtest` (or dump the parsed DERP section) before restarting the service
Example fix
# before
derp:
server:
enabled: true
# stun_addr missing
# after
derp:
server:
enabled: true
region_id: 999
region_code: headscale
region_name: Headscale Embedded DERP
stun_addr: 0.0.0.0:3478 Defensive patterns
Strategy: validation
Validate before calling
# before starting headscale, assert the DERP/STUN config pair:
if grep -q 'enabled: true' <<< "$(yq '.derp.server' /etc/headscale/config.yaml)"; then
[ -n "$(yq '.derp.server.stun_addr' /etc/headscale/config.yaml)" ] \
|| { echo 'derp.server.stun_addr must be set when derp.server.enabled'; exit 1; }
fi Try / catch
if err := h.Serve(); err != nil {
if errors.Is(err, errSTUNAddressNotSet) {
// config bug: fail fast with a clear operator message
log.Fatal().Msg("set derp.server.stun_addr (e.g. 0.0.0.0:3478) or disable derp.server")
}
return err
} Prevention
- Keep derp.server.enabled and derp.server.stun_addr in the same config block so they change together
- Run `headscale configtest` (or parse the config in CI) before deploying
- Document the STUN UDP port in firewall provisioning next to the DERP TCP port
When it happens
Trigger: Config file with derp.server.enabled: true and derp.server.stun_addr unset or "". Any invocation of headscale serve (or NewHeadscaleApp + Serve) with that combination returns errSTUNAddressNotSet at hscontrol/app.go:551-553.
Common situations: Enabling the embedded DERP server after a minimal config generated from an old example; YAML indentation mistakes that leave stun_addr nested under the wrong key; copying a config that only uses external DERP relays and then flipping server.enabled without adding the STUN block.
Related errors
- initial DERPMap is empty, Headscale requires at least one en
- 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/7e956c7beb7282c1.
Report an issue: GitHub.