Two gates
The installer does not write a lock file. It uses two gates: a database row written after the first run, and an env retire that opens the app for good. Both must be cleared before the panel is reachable.
The two gates
- Database lock. After
migrate→ configured seeders → optional first user, the controller callsInstallerState::lock(), which inserts one row into theinstaller_lockstable (id,installed_at). That row is the lock. Because it is a DB row, every replica sees the same state — there is no file on a local disk to drift out of sync. - Env retire. The
RedirectToInstallermiddleware short-circuits tonext($request)wheneverInstallerState::isRetired()is true, i.e.INSTALLER_ENABLED=false. Set that env on the host (Magic Containers env, or your local.env), redeploy, and the middleware stops redirecting. The app is open.
States and behaviour
The middleware reads the env first, then the controller reads the lock. The combined behaviour:
INSTALLER_ENABLED |
installer_locks row |
Behaviour |
|---|---|---|
true |
no | Checklist + Run (installer::install view) |
true |
set | Complete page — asks for INSTALLER_ENABLED=false, then Check |
false |
— | Middleware off; app open. /install redirects to / |
The Complete page (installer::complete) is what the operator sees between the lock being written and the env being flipped. Its only action is Check, which re-reads the env: if still true, it bounces back with an error; if false, it redirects to / with "Installer retired. App is open."
Why a DB row, not a file
Magic Containers run on ephemeral disks with no persistent volumes for stateless apps. A file written to storage/app on pod A is invisible to pod B and disappears on the next deploy. The installer_locks row lives in the shared libSQL database (Bunny DB / Turso / self-hosted sqld), so every replica reads the same answer to "is this app installed?".
State diagram
stateDiagram-v2
[*] --> Checklist: INSTALLER_ENABLED=true, no lock
Checklist --> Run: checks pass
Run --> Complete: migrate+seed+user → lock row written
Complete --> Complete: Check, env still true
Complete --> Open: INSTALLER_ENABLED=false + redeploy + Check
Open --> [*]: middleware off, app open