juanfont/headscale · critical
creating index: %w
Error message
creating index: %w
What it means
The final step of the SQLite schema-recreation migration failed creating the canonical indexes, including UNIQUE indexes on api_keys(prefix), users.provider_identifier, and the partial unique index on users.name for local accounts. Unlike the data-copy step, this fails when the CREATE INDEX statement itself conflicts - most often because a leftover index with the same name survived, or (for unique indexes) duplicate data slipped in.
Source
Thrown at hscontrol/db/db.go:432
"CREATE INDEX idx_users_deleted_at ON users(deleted_at)",
`CREATE UNIQUE INDEX idx_provider_identifier ON users(
provider_identifier
) WHERE provider_identifier IS NOT NULL`,
`CREATE UNIQUE INDEX idx_name_provider_identifier ON users(
name,
provider_identifier
)`,
`CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users(
name
) WHERE provider_identifier IS NULL`,
"CREATE UNIQUE INDEX idx_api_keys_prefix ON api_keys(prefix)",
"CREATE INDEX idx_policies_deleted_at ON policies(deleted_at)",
}
for _, indexSQL := range indexes {
err := tx.Exec(indexSQL).Error
if err != nil {
return fmt.Errorf("creating index: %w", err)
}
}
// Drop old tables only after everything succeeds
for _, table := range tablesToRename {
err := tx.Exec("DROP TABLE IF EXISTS " + table + "_old").Error
if err != nil {
log.Warn().Str("table", table+"_old").Err(err).Msg("failed to drop old table, but migration succeeded")
}
}
log.Info().Msg("schema recreation completed successfully")
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
// v0.27.1View on GitHub (pinned to 565fd254d0)
Solutions
- Check the wrapped error: 'index idx_... already exists' vs 'UNIQUE constraint failed'.
- For a name collision, open the DB with sqlite3 and DROP the leftover index, then restart headscale.
- For duplicate data, restore from backup, deduplicate rows, and retry the migration.
- If the migration otherwise completed, verify table data counts against users_old/nodes_old before dropping anything manually.
Example fix
-- before: retry fails with 'index idx_provider_identifier already exists' -- after: drop the leftover index, then restart headscale DROP INDEX IF EXISTS idx_provider_identifier; -- then: headscale serve / restart the server to re-run the migration
Defensive patterns
Strategy: fallback
Validate before calling
-- Pre-upgrade: confirm no leftover indexes from prior failed runs
SELECT name FROM sqlite_master WHERE type='index' AND name IN
('idx_users_deleted_at','idx_provider_identifier','idx_name_provider_identifier',
'idx_name_no_provider_identifier','idx_api_keys_prefix','idx_policies_deleted_at'); Try / catch
if _, err := db.NewHeadscaleDatabase(cfg); err != nil {
if strings.Contains(err.Error(), "creating index") {
if strings.Contains(err.Error(), "already exists") {
// DROP the leftover index via sqlite3, then restart headscale
} else if strings.Contains(err.Error(), "UNIQUE") {
// duplicate column data: restore backup, deduplicate, retry
}
}
} Prevention
- After a failed upgrade attempt, restore the backup instead of retrying against residual indexes.
- Verify uniqueness of api_keys.prefix and users identifiers before the schema-recreation migration.
- Confirm the server starts and serves requests before deleting the pre-upgrade backup.
When it happens
Trigger: CREATE UNIQUE INDEX rejected due to an existing index of the same name from a partially-completed earlier run, or duplicate values in the column after the data copy (forked/legacy data).
Common situations: Retry after a crashed migration leaving renamed tables AND old indexes; duplicate provider identifiers surviving the 202505141324 cleanup in edge cases.
Related errors
- copying data: %w
- foreign key constraints violated
- automigrating types.Route: %w
- automigrating types.Node: %w
- setting auth_key to null on nodes with non-existing keys: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/d81768c4f06105ca.
Report an issue: GitHub.