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 not

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Read the wrapped error — it identifies which migration or phase failed; fix that specific cause (lock, permissions, corrupt table).
  2. If you downgraded headscale, restore the binary version that matches the database's schema_migrations contents.
  3. For an interrupted migration on sqlite, restore from backup/litestream replica rather than hand-editing schema_migrations.
  4. Run with a fresh database to confirm the migration set itself is healthy, then compare against the failing DB.
  5. 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

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


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