Browse docs All docs
Docs / Named environments reference
Guidedeploymill://guides/named-environments

Named environments

A named environment is a permanent preview tagged with a stable name, the way you give an app a first-class staging or qa target instead of a throwaway PR preview. It is a regular preview app (it clones the parent's repo source, build type, container port, and env blob, and gets the same per-preview isolation: its own DB branch, bucket, and volumes), with two differences from an ephemeral preview:

  • It never auto-expires (ttlHours: null), so it survives the TTL cleanup sweep until you tear it down on purpose.
  • It carries an environmentName ("staging", "qa", …) that is unique per parent app, so the environment is addressable by a name a human or agent can reason about.

When to use a named environment vs an ephemeral preview

Use a named environment when…Use an ephemeral preview when…
You want a long-lived staging/qa/demo target that always existsYou're reviewing one PR/branch and want it cleaned up automatically
The environment is part of your release flow (deploy to staging, promote to prod)The deploy is throwaway and should self-reclaim
You want it declared in project.json and reconciled idempotentlyYou create it ad hoc with create_preview and a TTL

An ephemeral preview keeps its TTL countdown and is reaped by the cleanup sweep once createdAt + ttlHours is in the past. A named environment has no countdown. It is reclaimed only by an explicit delete_app.

Quota

A named environment is permanent, and permanent previews count against the standard app quota (like a prod app), not the separate preview quota. (An ephemeral, TTL'd preview draws from the preview quota.) So a named environment consumes a standard app slot. If the standard bucket is full, create_preview fails with errorCode: "active_app_limit_reached". Tear environments down with delete_app to free the slot.

Create one with create_preview

Pass environmentName. That single argument is what makes the preview a named environment: it forces the preview permanent (any ttlHours you pass is coerced to null). It is the only way to make a non-prod app permanent: an un-named preview is always ephemeral, and create_preview rejects a bare ttlHours: 0 with errorCode: "permanent_preview_requires_name". So reach for a name, not a zero TTL:

create_preview({
  parentApplicationId: "<parent-id>",
  ref: "staging",
  environmentName: "staging",
  // No ttlHours needed — environmentName alone makes the environment permanent
  database: "shared"   // optional — default is an isolated copy-on-write branch
})

The success response echoes environmentName alongside ttlHours: null and expiresAt: null, so an agent can confirm the tag landed:

{ "ok": true, "environmentName": "staging", "ttlHours": null, "expiresAt": null, "applicationId": "…", "url": "…" }

Name uniqueness

environmentName must be unique per parent app. Reusing a name that already exists on the same parent (even on a different ref) does not create a second environment. It returns a structured conflict the agent can branch on, and provisions nothing:

{ "ok": false, "errorCode": "environment_name_conflict", "error": "A named environment \"staging\" already exists on this app …", "existing": { "applicationId": "…", "ref": "staging", "environmentName": "staging" } }

To repoint an existing environment at a different branch, delete_app it and recreate it (a future revision may add an in-place repoint).

Declare environments in project.json

Add a top-level environments block to .deploymill/project.json and let reconcile_project manage them. The record key is the environment name; each entry has a ref (the branch it deploys from) and an optional shareDatabase:

{
  "version": 2,
  "name": "myapp",
  "workload": "web",
  "port": 3000,
  "domains": { "prod": "myapp.deploymill.app" },
  "rollback": "auto",
  "environments": {
    "staging": { "ref": "staging" },
    "qa":      { "ref": "qa", "shareDatabase": true }
  }
}
  • shareDatabase: false (the default) gives the environment its own isolated copy-on-write database branch, so destructive migrations stay off prod data.
  • shareDatabase: true clones the parent's DATABASE_URL so the environment reads/writes the parent's database (use only for non-migrating changes).

reconcile_project ensures each declared environment exists, idempotently:

  • If no named environment with that name exists on the app, it creates one (permanent, on the declared ref, with the declared database mode).
  • If it already exists, reconcile skips it: no churn, no redeploy.
  • A named environment that is live but not in config is left alone: reconcile never destructively deletes an environment you didn't declare.

What reconcile created this run is reported under applied.environmentsCreated (each entry carries environmentName, ref, and the new applicationId). A per-environment failure is surfaced as a warning rather than failing the whole reconcile, so the rest of the reconcile still lands and you can retry.

Teardown

Remove a named environment with delete_app, addressing it by its parent and ref (or by the preview's own applicationId):

delete_app({ parentApplicationId: "<parent-id>", ref: "staging" })

This tears down the environment app and, if it had its own isolated DB copy, drops that too, the same teardown an ephemeral preview gets. Deleting the parent app cascades to its named environments as well. To stop reconcile from recreating it, remove the entry from the environments block in project.json in the same change.

See also

  • deploymill://guides/previews: the full preview lifecycle (create/redeploy/delete/list, isolation, env handling).
  • deploymill://guides/project-config: the .deploymill/project.json contract.