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.1

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check the wrapped error: 'index idx_... already exists' vs 'UNIQUE constraint failed'.
  2. For a name collision, open the DB with sqlite3 and DROP the leftover index, then restart headscale.
  3. For duplicate data, restore from backup, deduplicate rows, and retry the migration.
  4. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/d81768c4f06105ca. Report an issue: GitHub.