Docs / Environment variables reference
Guidedeploymill://guides/env-vars

Environment variables reference (deploymill)

Reference for managing 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). Pass replace: true to overwrite the entire env blob.
  • list_env_vars — returns the env var names only (and a count). Values are write-only and can never be read back through any tool — there is no reveal option. Use it to see what's set, not to retrieve values.
  • 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_project set for you (notably DATABASE_URL from a provisioned database).
    • PREVIEW_URL (on preview apps — set by create_preview to the preview's full URL).
  • Keys you DO name in the call overwrite the existing value (or append if new).
  • replace: true flips 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 — list_env_vars is names-only).

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).

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 Neon database: create_preview forks a Neon branch off the parent's default branch and rewrites the preview's DATABASE_URL to point at the fork, so destructive migrations on the preview stay off prod data. The preview only shares the parent's DATABASE_URL when branching is opted out (previews.shareDatabase: true in .deploymill/project.json) or unavailable (no NEON_API_KEY, or the parent's database isn't Neon) — 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, then bind_secret or the secrets array in .deploymill/project.json. See deploymill://guides/secrets. set_env_vars is 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"] }), then deploy.
  • Inspect what's set: list_env_vars returns the key names (not values — values are write-only).
  • Bulk replace (rare): set_env_vars({ applicationId, env: {...}, replace: true }). Wipes everything not in the call, including DATABASE_URL, comments, and template references. 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_URL provisioned by reconcile_project should be left alone; if you need to rotate it, drop the database field and re-reconcile with prune: true, then re-add.
  • 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_vars is for non-secret config only.
  • Don't use replace: true to "clean up" an env you haven't read first. You will silently nuke DATABASE_URL and 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 deploy after set_env_vars. The container is still running with the old env.
  • DATABASE_URL disappeared → someone called set_env_vars with replace: true, or reconcile_project ran with prune: true after the database field was removed from .deploymill/project.json. Re-add database: { provider: "neon" } and re-reconcile to restore.
  • Comments / ${{project.X}} references gone after a merge → only happens with replace: true. Merge mode preserves them.