Runtime-wired DATABASE_URL — Conductor's dedicated-DB injection
Status: shipped (spec 26, Decision B). Architecture note.
The idea in one line
For a dedicated-DB app, Conductor derives DATABASE_URL from the app's provisioned dedicated container at deploy time — resolved on the box being deployed to — and never bakes it into the image, config, or repo. An explicit operator DATABASE_URL always wins.
Why it matters — the portability payoff
The app carries no database address of its own. Its connection is resolved at deploy from <slug>-db on whatever box it lands on. That's what makes an app a portable unit:
-
Move the app to another box → at deploy,
DATABASE_URLautomatically points at the dedicated DB on that box (same container name, different machine). Zero manualDATABASE_URLjuggling. This is precisely what makes app-transfer's cut-over clean (spec 26 / plan 04). -
Secret-safe by construction — the derived URL carries the password, so it's declared as a Kamal
env.secretand injected from.kamal/secrets; it's never in the image or the repo. - No drift — one source of truth (the provisioned container), not a hand-maintained connection string that rots.
How it works (the code)
-
App#deploy_env_pairs(server:)— the single source of what Conductor injects (.kamal/secrets, preflight, generated config). For a dedicated app it appends["DATABASE_URL", derived_database_url(server:)]. -
App#derived_database_url(server:)— the resolver:-
nilunlessdedicated_db? -
nilif the operator set aDATABASE_URLenv var (explicit always wins) - else
dedicated_database(server:).database_url→postgres://<user>:<pw>@<slug>-db:<port>/<name>, reached by container DNS on the app's docker network.
-
-
App#deploy_secret_keys(server:)— declaresDATABASE_URLas a Kamal secret when derived, so it's actually injected (without this, a written value is never injected and preflight misfires). -
Server-scoped
server:— the box being deployed to. During a transfer the app has a dedicated DB on both source and target (same container name); each deploy resolves its own box's DSN. The same app, two boxes, two DSNs — automatically.
The override (escape hatch)
Set DATABASE_URL as an app env var and it always wins — for an external/managed DB, a read-replica DSN, or anything non-standard. derived_database_url returns nil and the derived path steps aside. No special mode; just presence of the var.
How it relates
- Enables portable apps → app-transfer (spec 26 / plan 04): the target deploy derives the target box's DSN with no manual step.
- The shared→dedicated converter (
SharedToDedicatedConverter) removes the app's manualDATABASE_URLenv var on purpose — precisely so this derived path takes over once the app is dedicated. - Contrast: a shared-cluster app has a hand-set
DATABASE_URLenv var pointing at the shared Postgres — no derivation, and (today) not transferable until converted.
Gotchas worth knowing
- If a dedicated app also has a manual
DATABASE_URLenv var, the manual one wins — so a half-converted app can silently keep talking to the old DB. The converter deletes the manual var to avoid exactly this. - The derived URL is per-server; reading it without a
server:uses the app's current server. During a transfer, always pass the box you mean.