juanfont/headscale · critical
migration failed: %w
Error message
migration failed: %w
What it means
Top-level wrapper around runMigrations(), which executes gormigrate migrations (and, on sqlite, the FK-disabled early phase and the final foreign-key check). Any failure in any individual migration, the InitSchema AutoMigrate step, or the sqlite constraint-violation sweep surfaces here. It is the generic 'schema did not reach the expected state' error for headscale startup.
Source
Thrown at hscontrol/db/db.go:994
`CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users(name) WHERE provider_identifier IS NULL`,
`CREATE UNIQUE INDEX idx_pre_auth_keys_prefix ON pre_auth_keys(prefix) WHERE prefix IS NOT NULL AND prefix != ''`,
`CREATE UNIQUE INDEX idx_oauth_clients_client_id ON oauth_clients(client_id)`,
`CREATE UNIQUE INDEX idx_oauth_access_tokens_prefix ON oauth_access_tokens(prefix)`,
}
for _, indexSQL := range indexes {
err := tx.Exec(indexSQL).Error
if err != nil {
return err
}
}
return nil
})
err = runMigrations(cfg.Database, dbConn, migrations)
if err != nil {
return nil, fmt.Errorf("migration failed: %w", err)
}
// Store the current version in the database after migrations succeed.
// Dev builds skip this to preserve the stored version for the next
// real versioned binary.
currentVersion := types.GetVersionInfo().Version
if !isDev(currentVersion) {
err = setDatabaseVersion(dbConn, currentVersion)
if err != nil {
return nil, fmt.Errorf(
"storing database version: %w",
err,
)
}
}
// Validate that the schema ends up in the expected state.
// This is currently only done on sqlite as squibble does notView on GitHub (pinned to 565fd254d0)
Solutions
- Read the wrapped error — it identifies which migration or phase failed; fix that specific cause (lock, permissions, corrupt table).
- If you downgraded headscale, restore the binary version that matches the database's schema_migrations contents.
- For an interrupted migration on sqlite, restore from backup/litestream replica rather than hand-editing schema_migrations.
- Run with a fresh database to confirm the migration set itself is healthy, then compare against the failing DB.
- Never reorder or edit committed migrations — migration order is immutable (see AGENTS.md database rules).
Defensive patterns
Strategy: retry
Try / catch
// Treat startup migration failure as fatal unless the chained error is a // lock/busy condition. In an orchestrator: // if strings.Contains(err.Error(), "database is locked") -> restart with backoff // otherwise -> halt and page, restoring from backup.
Prevention
- Back up the database before upgrading headscale.
- Never run an older binary against a newer schema (downgrades are unsupported).
- Verify disk space and write permissions before upgrades.
- For sqlite, keep litestream in async replication mode so it does not hold write locks.
When it happens
Trigger: headscale start with a database that fails any migration: new migration SQL error (see error 400), MigrateTo('202501311657') failure, PRAGMA foreign_keys execution failure, or leftover foreign-key violations detected at the end of the sqlite migration run.
Common situations: Version downgrade (running an older binary against a DB migrated by a newer one — gormigrate rejects unknown migrations), interrupted migration leaving the schema_migrations table inconsistent, or a copy of the DB with a different collation/encoding.
Related errors
- automigrating types.Route: %w
- adding prefix column: %w
- adding hash column: %w
- version check: %w
- automigrating types.PreAuthKey: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/5070805f2ecd5bd5.
Report an issue: GitHub.