Environment variables
Manage per-app environment variables via set_env_vars, list_env_vars, and delete_env_vars. The platform stores env as a single per-application dotenv-style blob; these tools are the only supported way to mutate it.
The three tools
set_env_vars: add or update one or more keys. Merges into the existing env by default (preserving keys, comments, and${{project.X}}template references not named in the call). Passreplace: trueto overwrite the entire env blob.list_env_vars: returns the env var names and their provenance — never values. Values are write-only and can never be read back through any tool; there is no reveal option. Alongsidekeysandcountit returnssources[], one entry per key withsource("app-secret"= the app's own encrypted secret,"org-shared"= synced from the org vault,"app-override"= an app value shadowing an org-shared secret of the same name,"app-plain"= a plain env var DeployMill doesn't manage), plus the two axes as their own booleans,sensitiveandshared, and the vaultsecretname when there is one. Branch onsource/sensitiverather than guessing from the key name. Seedeploymill://guides/secretsfor how the vault-backed sources get there.delete_env_vars: remove one or more keys by name. Reports which keys were removed vs not found.
Merge semantics (the important bit)
set_env_vars is merge-by-default, not replace. That means:
- Keys you don't name in the call are left untouched, including:
- Comment lines (
# this is preserved). - Blank lines.
- Template references like
DATABASE_URL=${{project.DATABASE_URL}}(project-level shared env). - Anything
reconcile_projectwrote into the blob for you. PREVIEW_URL(on preview apps, set bycreate_previewto the preview's full URL).
- Comment lines (
- Keys you DO name in the call overwrite the existing value (or append if new).
replace: trueflips this. The entire blob is overwritten with exactly what you passed. Almost always wrong unless you're intentionally rewriting the whole env (note you can't read existing values back to reconstruct them, sincelist_env_varsnever returns values).
If you need to remove a key, use delete_env_vars. Don't try to do it by passing an empty string to set_env_vars (you'll just set the key to the empty string).
A managed DATABASE_URL is a vault secret, not an env-blob key
When the server has the secrets vault configured (production does), the DATABASE_URL for a managed database is stored as an app-scoped secret, not in the editable env blob — and any stale copy of it in the blob is stripped when it's written. It's injected into the container from the vault at deploy time, over the same channel as your other app secrets, so its value never sits in the blob and never crosses the agent. list_env_vars still shows the key, tagged source: "app-secret", sensitive: true.
Two practical consequences:
replace: truedoes not wipe it. It isn't a blob key, so rewriting the blob leaves it in place.delete_env_varsdoes not remove it either. That tool only edits the blob, so the key comes back inmissing. To get rid of a managed database's URL, drop thedatabaseblock and reconcile withprune: true.
Only on a server with no vault configured (local dev/test without SECRETS_ENC_KEY) does the managed DATABASE_URL fall back to the plain env blob, where the blob rules above apply to it. A DATABASE_URL you set yourself with set_env_vars (bring-your-own database) is always a plain blob key.
Changes require a redeploy
Env changes only take effect on the next deploy. The tools mutate the platform's stored env blob but the running container won't see the new values until it restarts. Call deploy after mutating env.
reconcile_project returns a note flagging when a redeploy is required for its changes (e.g. DATABASE_URL set during database provisioning). The same applies here. There's no auto-restart.
Template references
The platform supports ${{project.<KEY>}} as a reference to a project-level shared env var, e.g. DATABASE_URL=${{project.SHARED_DB_URL}}. DeployMill doesn't currently expose project-level env via MCP tools, but if a project-level env has been set out-of-band, those references survive merges as long as you don't name the same key in set_env_vars.
Previews get their own env blob
Each preview app is a regular DeployMill app with its own env. create_preview clones the parent's env at creation time and then applies the caller's envOverrides (with ${PREVIEW_URL} substitution for host-pinned vars). After creation, set_env_vars against the preview's applicationId modifies only that preview.
Note that DATABASE_URL is handled specially when the parent declares a managed database: create_preview gives the preview its own isolated copy of the database and rewrites the preview's DATABASE_URL to point at it, so destructive migrations on the preview stay off prod data. The copy is made with pg_dump while the parent keeps serving. The preview only shares the parent's DATABASE_URL when isolation is opted out (previews.shareDatabase: true in .deploymill/project.json) or unavailable (the database backend isn't configured, or the parent's DATABASE_URL points at a database DeployMill doesn't operate). create_preview's response reports which path it took via database.action and a matching warning. See deploymill://guides/previews for the full matrix.
Common operations
- Add a third-party API key / any secret: do NOT use
set_env_vars. Secret values must not pass through the agent. Use the org secrets vault:request_secret({ name })to get a browser link the human fills in, thenbind_secretor thesecretsarray in.deploymill/project.json. Seedeploymill://guides/secrets.set_env_varsis for non-secret config only (ports, flags, public URLs). - Update an existing key: same call. Merge semantics overwrite that single key in place.
- Remove a key:
delete_env_vars({ applicationId, keys: ["LEGACY_FLAG"] }), thendeploy. - Inspect what's set:
list_env_varsreturns the key names plus each key'ssource/sensitive/sharedprovenance (never values, which are write-only). - Bulk replace (rare):
set_env_vars({ applicationId, vars: {...}, replace: true }). Wipes everything in the blob that isn't in the call — comments, template references, and anyDATABASE_URLyou set yourself. A managedDATABASE_URLsurvives (it's a vault app-secret, not a blob key). Reach for this only when intentionally rewriting the whole env.
What NOT to do
- Don't edit env out-of-band for keys DeployMill manages.
DATABASE_URLprovisioned byreconcile_projectshould be left alone; if you need to rotate it, drop thedatabasefield and re-reconcile withprune: true, then re-add. - The managed object-storage keys are reserved. You can't set them.
set_env_varsrejects the whole call (atomically, nothing is written) witherrorCode: "reserved_env_key"ifvarscontains any ofS3_ENDPOINT,S3_REGION,S3_BUCKET,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEY, orS3_PREFIX. These are injected byreconcile_projectwhen the app declares object storage; letting a tenant rewriteS3_BUCKETwould repoint the app at another bucket. (UnlikeDATABASE_URL, which you can set for a bring-your-own-DB workflow, the S3 keys are never settable.) - Never pass secrets through
set_env_vars. Its values flow through the agent and may land in a transcript. Anything sensitive goes through the secrets vault (request_secret→ browser entry →bind_secret), where the value never touches the agent.set_env_varsis for non-secret config only. - Don't use
replace: trueto "clean up" an env you haven't read first. You will silently nuke any self-setDATABASE_URLand any project-level template references. - Don't expect env changes to apply without a redeploy. They don't.
Troubleshooting
- New key not visible inside the app → you didn't
deployafterset_env_vars. The container is still running with the old env. DATABASE_URLdisappeared → for a managed database the usual cause isreconcile_projectrunning withprune: trueafter thedatabasefield was removed from.deploymill/project.json; re-adddatabase: { engine: "postgres" }and re-reconcile to restore.set_env_vars({ replace: true })can only have done it for aDATABASE_URLyou set yourself (a managed one lives in the vault, not the blob). Iflist_env_varsstill shows it withsource: "app-secret"but the app can't see it, you haven't redeployed since it was written.- Comments /
${{project.X}}references gone after a merge → only happens withreplace: true. Merge mode preserves them.