{"record":{"id":"59de90a5345cebe4","repo":"juanfont/headscale","slug":"storing-database-version-w","errorCode":null,"errorMessage":"storing database version: %w","messagePattern":"storing database version: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/db/db.go","lineNumber":1004,"sourceCode":"\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\terr = runMigrations(cfg.Database, dbConn, migrations)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"migration failed: %w\", err)\n\t}\n\n\t// Store the current version in the database after migrations succeed.\n\t// Dev builds skip this to preserve the stored version for the next\n\t// real versioned binary.\n\tcurrentVersion := types.GetVersionInfo().Version\n\tif !isDev(currentVersion) {\n\t\terr = setDatabaseVersion(dbConn, currentVersion)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"storing database version: %w\",\n\t\t\t\terr,\n\t\t\t)\n\t\t}\n\t}\n\n\t// Validate that the schema ends up in the expected state.\n\t// This is currently only done on sqlite as squibble does not\n\t// support Postgres and we use our sqlite schema as our source of\n\t// truth.\n\tif cfg.Database.Type == types.DatabaseSqlite {\n\t\tsqlConn, err := dbConn.DB()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"getting DB from gorm: %w\", err)\n\t\t}\n\n\t\t// or else it blocks...\n\t\tsqlConn.SetMaxIdleConns(maxIdleConns)","sourceCodeStart":986,"sourceCodeEnd":1022,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L986-L1022","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"-- Postgres: confirm the headscale role can write metadata before deploy\n-- psql as the headscale user:\nSELECT has_table_privilege(current_user, 'public', 'INSERT');","typeGuard":null,"tryCatchPattern":"// Safe to retry: migrations already succeeded and are idempotent.\n// Restart headscale after fixing the underlying write permission; the\n// version-stamping write is re-attempted on every non-dev startup.","preventionTips":["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."],"tags":["database","migration","metadata","permissions"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}