juanfont/headscale · error
storing database version: %w
Error message
storing database version: %w
What it means
After all migrations succeed, headscale records the running binary's version in the database (setDatabaseVersion) so future upgrades can decide what to migrate. Dev builds skip this deliberately. This error means migrations completed but the version bookkeeping write failed — the schema is fine, the metadata write is not.
Source
Thrown at hscontrol/db/db.go:1004
}
}
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
// support Postgres and we use our sqlite schema as our source of
// truth.
if cfg.Database.Type == types.DatabaseSqlite {
sqlConn, err := dbConn.DB()
if err != nil {
return nil, fmt.Errorf("getting DB from gorm: %w", err)
}
// or else it blocks...
sqlConn.SetMaxIdleConns(maxIdleConns)View on GitHub (pinned to 565fd254d0)
Solutions
- Grant the headscale DB user INSERT/UPDATE on the database (or at minimum the version table) and restart.
- Check the chained error for the exact SQL problem; verify the table setDatabaseVersion writes to exists.
- If using sqlite, confirm the file and directory are writable by the headscale process user.
- Safe to simply restart headscale — migrations already succeeded and are idempotent, so the version write will be retried.
Defensive patterns
Strategy: retry
Validate before calling
-- Postgres: confirm the headscale role can write metadata before deploy -- psql as the headscale user: SELECT has_table_privilege(current_user, 'public', 'INSERT');
Try / catch
// Safe to retry: migrations already succeeded and are idempotent. // Restart headscale after fixing the underlying write permission; the // version-stamping write is re-attempted on every non-dev startup.
Prevention
- Grant the headscale DB user full DML rights on its schema.
- Verify volume writability (mount checks in container probes).
- Avoid mixing dev builds with production databases.
When it happens
Trigger: setDatabaseVersion fails: write to the version/metadata table blocked by permissions, table missing because InitSchema partially ran, connection dropped right after migrations completed, or a NOT NULL/constraint violation on the version row.
Common situations: Database user lacks UPDATE/INSERT rights on the metadata table (common in hardened postgres setups); sqlite file on a read-only mount after migrations ran from cache; a crash-restart race where the table exists but is locked.
Related errors
- foreign key constraints violated
- version check: %w
- automigrating types.Route: %w
- automigrating types.PreAuthKey: %w
- automigrating types.Node: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/59de90a5345cebe4.
Report an issue: GitHub.