juanfont/headscale · critical
init state: %w
Error message
init state: %w
What it means
state.NewState(cfg) failed during headscale startup. State is the central coordinator (per AGENTS.md, all cross-subsystem operations go through hscontrol/state); its constructor opens the database, runs migrations, seeds policy, and initializes the in-memory NodeStore. The wrapped error identifies which stage broke — most commonly database open/migration (bad db.type/db.path, unreachable PostgreSQL, failed migration) or invalid policy/config values validated at construction time.
Source
Thrown at hscontrol/app.go:125
tailsqlTSKey = envknob.String("TS_AUTHKEY")
dumpConfig = envknob.Bool("HEADSCALE_DEBUG_DUMP_CONFIG")
)
func NewHeadscale(cfg *types.Config) (*Headscale, error) {
var err error
if profilingEnabled {
runtime.SetBlockProfileRate(1)
}
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("reading or creating Noise protocol private key: %w", err)
}
s, err := state.NewState(cfg)
if err != nil {
return nil, fmt.Errorf("init state: %w", err)
}
app := Headscale{
cfg: cfg,
noisePrivateKey: noisePrivateKey,
clientStreamsOpen: sync.WaitGroup{},
state: s,
}
if len(cfg.TrustedProxies) > 0 {
app.realIPMiddleware, err = trustedProxyRealIP(cfg.TrustedProxies)
if err != nil {
return nil, fmt.Errorf("building trusted_proxies middleware: %w", err)
}
}
// Initialize ephemeral garbage collector
ephemeralGC := db.NewEphemeralGarbageCollector(func(ni types.NodeID) {View on GitHub (pinned to 565fd254d0)
Solutions
- Read the wrapped error — 'init state' chains the real cause (db open, migration name, policy parse)
- For PostgreSQL: verify connectivity with the same DSN (`psql <dsn> -c 'select 1'`) and credentials
- For SQLite: ensure db_path's directory exists and is writable by the headscale user; remove stale lock files from a crashed instance
- Restore from backup if a migration fails mid-way; never skip failed migrations (migration rules in AGENTS.md are load-bearing)
Defensive patterns
Strategy: try-catch
Validate before calling
// For PostgreSQL configs, verify connectivity before handing off to headscale
if cfg.Database.Type == "postgres" {
if err := pingDB(cfg.Database.Postgres.Host, cfg.Database.Postgres.User); err != nil {
return fmt.Errorf("database unreachable: %w", err)
}
} Try / catch
if _, err := state.NewState(cfg); err != nil {
// unwrapping reveals db open vs migration vs policy validation;
// migration failures must never be retried with FKs disabled or orders changed
return fmt.Errorf("init state: %w", err)
} Prevention
- Start/health-check the database before headscale (init ordering in containers/Pods)
- Back up the database before upgrading headscale versions
- Keep db.path on a writable persistent volume owned by the headscale user
When it happens
Trigger: Configured database unavailable (PostgreSQL connection refused, wrong credentials), SQLite db_path in an unwritable location, a migration failing against a corrupted or partially-upgraded schema, or policy/config validation rejecting values inside NewState.
Common situations: PostgreSQL not started before headscale; db path on a read-only volume; upgrading headscale across versions against an old database whose schema was hand-modified; wrong db.type spelling in config; SQLite file locked by another headscale instance.
Related errors
- refreshing NodeStore after IP backfill: %w
- database type not supported
- configuration error: %w
- building trusted_proxies middleware: %w
- DERP server private key and noise private key are the same:
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/657660a81d085773.
Report an issue: GitHub.