juanfont/headscale · critical
dropping routes table: %w
Error message
dropping routes table: %w
What it means
During the v0.27.0 SQLite-only schema recreation migration, DROP TABLE routes failed. The migration drops a leftover routes table (normally already removed by migration 202502131714) before renaming and rebuilding all core tables. Failure means SQLite rejected the DROP: database locked by another connection, read-only file, or insufficient permissions.
Source
Thrown at hscontrol/db/db.go:269
log.Info().Msg("skipping schema migration on non-SQLite database")
return nil
}
log.Info().Msg("starting schema recreation with table renaming")
// Rename existing tables to _old versions
tablesToRename := []string{"users", "pre_auth_keys", "api_keys", "nodes", "policies"}
// Check if routes table exists and drop it (should have been migrated already)
var routesExists bool
err := tx.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='routes'").Row().Scan(&routesExists)
if err == nil && routesExists {
log.Info().Msg("dropping leftover routes table")
err := tx.Exec("DROP TABLE routes").Error
if err != nil {
return fmt.Errorf("dropping routes table: %w", err)
}
}
// Drop all indexes first to avoid conflicts
indexesToDrop := []string{
"idx_users_deleted_at",
"idx_provider_identifier",
"idx_name_provider_identifier",
"idx_name_no_provider_identifier",
"idx_api_keys_prefix",
"idx_policies_deleted_at",
}
for _, index := range indexesToDrop {
_ = tx.Exec("DROP INDEX IF EXISTS " + index).Error
}
for _, table := range tablesToRename {View on GitHub (pinned to 565fd254d0)
Solutions
- Ensure no other process has the SQLite file open: stop old containers/instances and CLI commands.
- Verify write permission on both the database file and its directory.
- Retry startup - the existence check and DROP are idempotent.
- If it persists, open the DB with the sqlite3 shell and DROP TABLE routes manually (data was already migrated), then restart.
Defensive patterns
Strategy: try-catch
Try / catch
if _, err := db.NewHeadscaleDatabase(cfg); err != nil {
if strings.Contains(err.Error(), "dropping routes table") {
// SQLite lock or read-only file: stop other sqlite users, fix perms, restart
}
} Prevention
- Stop the old headscale process completely before starting the upgraded one.
- Mount the SQLite volume read-write; the migration needs journal + DDL access.
- Never open the production DB in an editor/shell while headscale starts.
When it happens
Trigger: Another process (old headscale instance, CLI, sqlite3 shell) holds the database while startup migration runs; SQLite file or directory not writable (journal creation fails).
Common situations: Restarting headscale while the previous instance has not fully exited; database on NFS without proper locking; container running with a read-only mounted volume.
Related errors
- renaming table %s to %s_old: %w
- setting auth_key to null on nodes with non-existing keys: %w
- adding column types.Node: %w
- creating new table: %w
- foreign key constraints violated
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/ddd9e12526cc31b1.
Report an issue: GitHub.