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
- Read the 'Foreign key constraint violated' log lines immediately above the error to identify table/row_id/parent
- Restore from a clean backup taken before the corruption
- Repair by deleting the orphaned child rows (e.g. nodes whose user_id has no matching users row) after backing up the SQLite file
- 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
- Back up the SQLite file before every headscale upgrade
- Never insert child rows (nodes, routes, keys) with direct SQL
- Periodically run PRAGMA foreign_key_check on production databases
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
- path cannot be empty
- automigrating types.Route: %w
- automigrating types.Node: %w
- setting auth_key to null on nodes with non-existing keys: %w
- adding column types.Node: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/0a06172b4999e7ff.
Report an issue: GitHub.