juanfont/headscale · critical
renaming forced_tags to tags
Error message
renaming forced_tags to tags: %w
What it means
Migration '202511131445-node-forced-tags-to-tags' fails when GORM Migrator().RenameColumn cannot rename nodes.forced_tags to nodes.tags. Typical causes: the forced_tags column does not exist (schema already has tags, e.g. from a newer AutoMigrate run or a partially applied upgrade), the DB lacks ALTER rights, or the table is locked. Unlike the AddColumn guards above, RenameColumn here has no HasColumn check and is not idempotent.
Solutions
- Verify the actual schema: \d nodes (Postgres) or PRAGMA table_info(nodes) (SQLite) - if 'tags' exists and 'forced_tags' does not, the rename is already done; repair the migrations bookkeeping or pre-rename manually so the call succeeds
- If both columns exist, copy data: UPDATE nodes SET tags = forced_tags; then drop forced_tags, and restart
- Check the wrapped error for permissions/locks (grant ALTER, stop other instances)
- Never downgrade headscale across this boundary - restore from a backup taken before the version jump if state is inconsistent
Example fix
-- before: migrations table expects rename but schema already has tags ALTER TABLE nodes RENAME COLUMN forced_tags TO tags; -- fails: no such column -- after: reconcile schema with history, or complete the rename manually UPDATE nodes SET tags = forced_tags WHERE tags IS NULL; ALTER TABLE nodes DROP COLUMN forced_tags;
Defensive patterns
Strategy: type-guard
Validate before calling
// Verify column state before upgrading across this boundary
cols := map[string]bool{}
rows, _ := db.Query("PRAGMA table_info(nodes)") // or information_schema.columns on Postgres
for rows.Next() {
var cid int; var name, typ string; var notnull, pk int; var dflt any
rows.Scan(&cid, &name, &typ, ¬null, &dflt, &pk)
cols[name] = true
}
if cols["forced_tags"] && cols["tags"] {
log.Fatal("both forced_tags and tags exist; reconcile manually before upgrading")
}
if !cols["forced_tags"] && !cols["tags"] {
log.Fatal("nodes has neither forced_tags nor tags; schema/history mismatch - restore from backup")
} Type guard
// Go helper to assert the rename is possible
func columnExists(db *sql.DB, table, column string) bool {
var n int
db.QueryRow(`SELECT count(*) FROM pragma_table_info(?)`, table).Scan(&n) // SQLite
return n > 0
}
// guard: columnExists(db, "nodes", "forced_tags") must be true before this migration runs Prevention
- Never run an older headscale binary against a newer database (downgrade then upgrade causes exactly this)
- Keep migrations-table backups in sync with schema backups when dumping/restoring
- Check release notes for schema-changing releases before jumping multiple versions
When it happens
Trigger: Starting a headscale binary whose embedded schema (AutoMigrate-created nodes.tags) raced ahead of the migration history - for example restoring a migrations table from a newer backup onto an older schema, or running headscale versions out of order (downgrade then upgrade). Also ALTER privilege missing or SQLite lock held by another process.
Common situations: Rolling back to an old headscale version and then jumping forward again, leaving columns renamed but migrations unrecorded; restoring a database dump where the migrations table and schema disagree; two instances of different versions sharing one DB.
Related errors
- adding description to pre_auth_keys
- adding hash column
- adding prefix column
- adding user_id to api_keys
- automigrating types.Node
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/5502079db8372f34.
Report an issue: GitHub.
Appendix: source
Thrown at hscontrol/db/db.go:591
err := tx.Exec(createSQL).Error
if err != nil {
return fmt.Errorf("creating index: %w", err)
}
}
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
{
// Rename forced_tags column to tags in nodes table.
// This must run after migration 202505141324 which creates tables with forced_tags.
ID: "202511131445-node-forced-tags-to-tags",
Migrate: func(tx *gorm.DB) error {
// Rename the column from forced_tags to tags
err := tx.Migrator().RenameColumn(&types.Node{}, "forced_tags", "tags")
if err != nil {
return fmt.Errorf("renaming forced_tags to tags: %w", err)
}
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
{
// Migrate RequestTags from host_info JSON to tags column.
// In 0.27.x, tags from --advertise-tags (ValidTags) were stored only in
// host_info.RequestTags, not in the tags column (formerly forced_tags).
// This migration validates RequestTags against the policy's tagOwners
// and merges validated tags into the tags column.
// Fixes: https://github.com/juanfont/headscale/issues/3006
ID: "202601121700-migrate-hostinfo-request-tags",
Migrate: func(tx *gorm.DB) error {
// 1. Load policy from file or database based on configuration
policyData, err := PolicyBytes(tx, cfg)
if err != nil {View on GitHub (pinned to 565fd254d0)