juanfont/headscale · error
disabling foreign keys: %w
Error message
disabling foreign keys: %w
What it means
On sqlite, headscale runs the frozen set of pre-2025-07-02 early migrations with PRAGMA foreign_keys = OFF (these old automigrations cannot run with FKs enabled). This error means the PRAGMA statement itself failed — the connection could not even accept the pragma, typically because the database is locked or the connection is bad.
Source
Thrown at hscontrol/db/db.go:1170
return nil, fmt.Errorf(
"database of type %s is not supported: %w",
cfg.Type,
errDatabaseNotSupported,
)
}
func runMigrations(cfg types.DatabaseConfig, dbConn *gorm.DB, migrations *gormigrate.Gormigrate) error {
if cfg.Type == types.DatabaseSqlite {
// SQLite: Run the early migrations that GORM cannot handle safely with
// foreign keys enabled (route and pre-auth-key automigrations) with FK
// disabled, then run everything else with FK enabled.
//
// NO NEW MIGRATIONS SHOULD RUN WITH FK DISABLED. As of 2025-07-02, all
// new migrations must run with foreign keys enabled via the
// migrations.Migrate() call below.
if err := dbConn.Exec("PRAGMA foreign_keys = OFF").Error; err != nil { //nolint:noinlineerr
return fmt.Errorf("disabling foreign keys: %w", err)
}
// Run up to and including the last migration that requires FK disabled.
if err := migrations.MigrateTo("202501311657"); err != nil { //nolint:noinlineerr
return fmt.Errorf("running migration 202501311657: %w", err)
}
if err := dbConn.Exec("PRAGMA foreign_keys = ON").Error; err != nil { //nolint:noinlineerr
return fmt.Errorf("restoring foreign keys: %w", err)
}
// Run the rest of the migrations
if err := migrations.Migrate(); err != nil { //nolint:noinlineerr
return err
}
// Check for constraint violations at the end
type constraintViolation struct {View on GitHub (pinned to 565fd254d0)
Solutions
- Ensure exactly one headscale process opens the sqlite database; stop duplicates.
- Close interactive sqlite3 sessions and let litestream run in replication-only mode.
- Move the sqlite file off network filesystems onto local disk.
- If a stale -wal/-shm or lock persists after all holders are gone, copy the DB aside and verify integrity with sqlite3 db 'PRAGMA integrity_check;'.
Defensive patterns
Strategy: retry
Validate before calling
// Detect an existing holder before startup (sqlite): // lsof /var/lib/headscale/headscale.db # expect no output besides this process // fuser -v /var/lib/headscale/headscale.db
Try / catch
// If wrapped error contains 'database is locked', restart with backoff // after stopping the conflicting process. Otherwise investigate corruption.
Prevention
- One writer per sqlite file, always.
- Avoid NFS/network filesystems for sqlite.
- Set sqlite busy_timeout via config if transient locks are expected.
When it happens
Trigger: Another process holds an exclusive lock on the sqlite file at startup (second headscale instance, litestream in certain modes, an open sqlite3 shell in a transaction); or the file is corrupt enough that any statement fails.
Common situations: Accidentally running two headscale replicas against one sqlite file; a leftover lock file after a crash; the DB file on NFS or a filesystem without proper locking.
Related errors
- restoring foreign keys: %w
- automigrating types.Route: %w
- setting auth_key to null on nodes with non-existing keys: %w
- dropping routes table: %w
- checking if table %s exists: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/695ce66155aa20f9.
Report an issue: GitHub.