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

  1. Ensure exactly one headscale process opens the sqlite database; stop duplicates.
  2. Close interactive sqlite3 sessions and let litestream run in replication-only mode.
  3. Move the sqlite file off network filesystems onto local disk.
  4. 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

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


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