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

  1. Database lock. After migrate → configured seeders → optional first user, the controller calls InstallerState::lock(), which inserts one row into the installer_locks table (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.
  2. Env retire. The RedirectToInstaller middleware short-circuits to next($request) whenever InstallerState::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

See also

Built by Qcentic