configtest runs database migrations against the configured DB, not a pure config validator
Author: DavieyCreated Aug 30, 2026Updated Sep 1, 2026
What happened
headscale configtest is documented as "Run a test of the configuration and exit", but it constructs the full server, which opens the configured database and runs all pending migrations. We hit this operationally while staging an upgrade:
- Production server running X, live sqlite DB at schema X.
- Ran
configtestwith the Y (newer) binary against the same config/DB to pre-validate the config; it exited 0. - Rolled back the server to X for unrelated reasons and X refused to start: schema had been forward-migrated to Y by
configtestalone. (Migrations are forward-only, so rollback required a DB restore from snapshot.)
Code path (master @ cbe3030)
cmd/headscale/cli/configtest.go:RunEcallsnewHeadscaleServerWithConfig()cmd/headscale/cli/utils.go:89: same constructorserveuseshscontrol/app.go:NewHeadscale->state.NewState(cfg)hscontrol/state/state.go:228:hsdb.NewHeadscaleDatabase(cfg)opens the DB and runs gormigrate with the full migration list
So configtest is exactly serve minus the listen, including DB mutation. NewState also loads all nodes and marks them offline in memory; whether any of that persists depends on the gorm session, but the migrations definitely do.
Expected
Either:
configtestvalidates config (and policy/ACL syntax) without opening the database, or- it opens the DB read-only / skips migrations, or
- at minimum the help text warns that it mutates the configured database.
Happy to send a PR if there's agreement on which behaviour is intended. My vote is the first: validate config + policy parsing only, no DB, since that's what the command name and help promise.
Source: juanfont/headscale