juanfont/headscale · critical

version check

Error message

version check: %w

What it means

Returned by NewHeadscaleDatabase when checkVersionUpgradePath rejects the existing database version before any migration runs. Headscale only supports specific upgrade paths (e.g. pre-v0.25 databases must first be upgraded to v0.25.1, per the comment above the migration list); downgrades and multi-step jumps are refused to protect schema integrity.

Solutions

  1. Read the wrapped message to see the database's current version vs the supported range.
  2. To upgrade from pre-v0.25: first run v0.25.1 against the database, then the target version.
  3. To downgrade: restore a database backup taken before the upgrade - headscale will not migrate backwards.
  4. Never point two major versions at the same database file.
Defensive patterns

Strategy: try-catch

Try / catch

hsdb, err := db.NewHeadscaleDatabase(cfg)
if err != nil {
    if strings.Contains(err.Error(), "version check") {
        // database version is unsupported: stop, back up, and upgrade stepwise
        log.Fatal().Err(err).Msg("unsupported database version - upgrade via the documented path, do not delete the DB")
    }
    return err
}

Prevention

When it happens

Trigger: Starting a new headscale binary against a database written by a much older release (pre-v0.25), or pointing a newer binary at a database created by a NEWER version (downgrade). The wrapped error from versioncheck.go names the offending version.

Common situations: Rolling back a headscale upgrade while keeping the same database; restoring an old database backup onto a new binary; skipping several releases in one upgrade.

Related errors


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

Appendix: source

Thrown at hscontrol/db/db.go:63

type HSDatabase struct {
	DB  *gorm.DB
	cfg *types.Config
}

// NewHeadscaleDatabase creates a new database connection and runs migrations.
// It accepts the full configuration to allow migrations access to policy settings.
//
//nolint:gocyclo // complex database initialization with many migrations
func NewHeadscaleDatabase(cfg *types.Config) (*HSDatabase, error) {
	dbConn, err := openDB(cfg.Database)
	if err != nil {
		return nil, err
	}

	err = checkVersionUpgradePath(dbConn)
	if err != nil {
		return nil, fmt.Errorf("version check: %w", err)
	}

	migrations := gormigrate.New(
		dbConn,
		gormigrate.DefaultOptions,
		[]*gormigrate.Migration{
			// New migrations must be added as transactions at the end of this list.
			// Migrations start from v0.25.0. If upgrading from v0.24.x or earlier,
			// you must first upgrade to v0.25.1 before upgrading to this version.

			// v0.25.0
			{
				// Add a constraint to routes ensuring they cannot exist without a node.
				ID: "202501221827",
				Migrate: func(tx *gorm.DB) error {
					// Remove any invalid routes associated with a node that does not exist.
					if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) { //nolint:staticcheck // SA1019: Route kept for migrations
						err := tx.Exec("delete from routes where node_id not in (select id from nodes)").Error

View on GitHub (pinned to 565fd254d0)