Conductor · architecture · database

Your app carries no database address

For a dedicated-DB app, Conductor derives DATABASE_URL from the app's own provisioned container at deploy time — resolved on the box it's landing on — and never bakes it into the image, config, or repo. That one choice is what makes an app a portable unit.

status: shippedspec 26 · Decision Barchitecture note
In one lineDedicated app → DATABASE_URL is derived from <slug>-db at deploy, per box, secret-safe. An explicit operator DATABASE_URL always wins.

Why it matters — portability

The app holds no DB address. Its connection is resolved at deploy from the dedicated container on whatever box it lands on — so moving the app to another box just works: the deploy on the new box derives the new box's DSN automatically. Same container name, different machine, zero manual juggling. This is exactly what makes app-transfer's cut-over clean.

THE SAME APP, MID-TRANSFER — each box resolves its own DSN
source · pavelabs-multi
postgres://…@calm-page-db:5432/…
target · SSD-Node
postgres://…@calm-page-db:5432/…

Same <slug>-db container name on both boxes; the deploy resolves the DSN for the box it targets. No connection string is ever moved by hand.

How it works

App#derived_database_url(server:) returns the dedicated container's DSN — appended by deploy_env_pairs and declared secret by deploy_secret_keys (so it's injected from .kamal/secrets).

def derived_database_url(server: self.server)
  return nil unless dedicated_db?
  return nil if env_variables.any? { |v| v.key == "DATABASE_URL" }  # explicit wins
  dedicated_database(server:)&.database_url
end
# → postgres://<user>:<pw>@<slug>-db:<port>/<name>  (container DNS)

A shared-cluster app has a hand-set DATABASE_URL env var pointing at the shared Postgres. No derivation — and (today) not transferable until converted (SharedToDedicatedConverter) to the dedicated model.

Set DATABASE_URL as an app env var and it always wins — external/managed DB, a read-replica DSN, anything non-standard. derived_database_url returns nil and the derived path steps aside. No special mode; just presence of the var.

Gotchas

Read before relying on it
  • A dedicated app that also has a manual DATABASE_URL silently keeps talking to the old DB — the manual var wins. The converter deletes it on purpose.
  • The derived URL is per-server; without a server: it uses the app's current server. In a transfer, always pass the box you mean.

How it relates

The payoff chainDerived DSN → portable apps → app-transfer (spec 26 / plan 04). And SharedToDedicatedConverter deletes the manual DATABASE_URL precisely so this derived path takes over once an app becomes dedicated.