juanfont/headscale · critical
renaming forced_tags to tags: %w
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.
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)
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
- automigrating types.Route: %w
- automigrating types.PreAuthKey: %w
- automigrating types.Node: %w
- listing users: %w
- adding prefix column: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/5502079db8372f34.
Report an issue: GitHub.