juanfont/headscale · critical

foreign key constraints violated

Error message

foreign key constraints violated

What it means

Returned at the end of SQLite migrations (hscontrol/db/db.go:1218-1228) when PRAGMA foreign_key_check reports orphaned rows after the migration run. Each violation is logged with table, row_id, and parent table before the sentinel is returned, so the log lines immediately above the error identify the exact rows. It means the database contains rows referencing non-existent parents (e.g. a node with a user_id that has no users row).

Source

Thrown at hscontrol/db/db.go:38

	"github.com/juanfont/headscale/hscontrol/util"
	"github.com/rs/zerolog/log"
	"github.com/tailscale/squibble"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
)

//go:embed schema.sql
var dbSchema string

func init() {
	schema.RegisterSerializer("text", TextSerialiser{})
}

var errDatabaseNotSupported = errors.New("database type not supported")

var errForeignKeyConstraintsViolated = errors.New("foreign key constraints violated")

const (
	maxIdleConns   = 100
	maxOpenConns   = 100
	contextTimeout = 10 * time.Second
)

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)

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Read the 'Foreign key constraint violated' log lines immediately above the error to identify table/row_id/parent
  2. Restore from a clean backup taken before the corruption
  3. Repair by deleting the orphaned child rows (e.g. nodes whose user_id has no matching users row) after backing up the SQLite file
  4. If the DB is disposable, move/delete the sqlite file and let headscale recreate the schema

Example fix

-- identify orphans (example for nodes)
SELECT n.id, n.user_id FROM nodes n
LEFT JOIN users u ON u.id = n.user_id
WHERE u.id IS NULL;
-- after backing up, remove or repair them, then restart headscale
Defensive patterns

Strategy: validation

Validate before calling

-- run before upgrading/restarting against a suspect DB:
PRAGMA foreign_key_check;
-- if it returns rows, fix them before letting headscale migrate

Try / catch

if err := db.HeadscaleDBMigrations(...); err != nil {
    if errors.Is(err, errForeignKeyConstraintsViolated) {
        // log lines above list table/row_id/parent — route to a data-repair runbook
        return fmt.Errorf("orphaned rows detected; run PRAGMA foreign_key_check and repair: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Starting headscale against a SQLite file that was previously migrated with foreign keys disabled, hand-edited, partially restored from backup, or produced by a very old version with known FK gaps; the post-migration PRAGMA foreign_key_check then finds orphans.

Common situations: Restoring a dump that skipped parent tables; a crashed migration leaving half-written rows; databases touched by external scripts that inserted child rows directly.

Related errors


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