Environment Variables
Zerops manages environment variables at two scopes (project and service) with strict build/runtime isolation. Variables are set via zerops.yml, import.yml, or GUI. Project vars auto-inherit into every service — read them directly, no declaration. Cross-service (sibling) vars do NOT auto-inject under the default envIsolation=service — reference a sibling's value explicitly as ${hostname_varname} in run.envVariables (only legacy none mode injects siblings as bare vars). Secret reads are privilege-gated (an admin token returns the value; a read-only token gets REDACTED). A running process keeps its boot-time env — restart it (not reload) to pick up a changed value.
Scope Hierarchy
| Scope | Defined In | Visibility | Editable Without Redeploy |
|---|---|---|---|
| Project | import.yml project.envVariables, GUI | All services (auto-inherited) | Yes (restart to apply) |
| Service secret | import.yml envSecrets, GUI | Single service | Yes (restart to apply) |
| Service basic (build) | zerops.yml build.envVariables | Build container only | No (redeploy required) |
| Service basic (runtime) | zerops.yml run.envVariables | Runtime container only | No (redeploy required) |
Variable Precedence
Total order for the bare key (highest wins): system/platform > yaml-baked run.envVariables > service secret/userData > project.
- yaml-baked
run.envVariablesowns its key — a service secret/userData set on the same key is rejected (userDataDuplicateKey); the two never coexist. To change a yaml-baked value, editzerops.ymland redeploy. - Service-level wins over project-level.
- Build and runtime are separate environments -- same key can have different values in each.
DO NOT set a secret/service var on a key already declared in run.envVariables — the platform rejects it. The yaml var owns the key; edit the yaml and redeploy instead.
Build/Runtime Isolation
Build and runtime run in separate containers. Variables from one phase are not visible in the other unless explicitly referenced with prefixes:
| Want to access | From | How |
|---|---|---|
Runtime var API_KEY | Build container | ${RUNTIME_API_KEY} — runtime run.envVariables are known at build time, so the build can read them |
Build var BUILD_ID | Runtime container | Not available. The build container is destroyed after build and its vars are not carried into the runtime env store — ${BUILD_BUILD_ID} reaches the runtime process as the literal string ${BUILD_BUILD_ID}. Persist the value into a deployed file, or recompute it at runtime. |
Cross-Service References — Explicit ${hostname_varname} Required
Under the default envIsolation=service, a service does NOT automatically see another service's variables — not even a managed database's connection vars. To use a sibling's value, reference it explicitly in run.envVariables; the left-hand key is the name your app reads, the right-hand ${hostname_varname} is the source:
- The reference resolves at container start, independent of isolation mode — the referenced var does not need to exist at definition time.
- An unresolved ref stays literal (
${db_hostname}reaches the process verbatim) — no error, no blank. A wrong hostname/var on the right-hand side becomes a literal string and the app fails at connect time. - Hostname charset: service hostnames are lowercase alphanumeric only (
[a-z0-9]) — the platform rejects dashes, underscores, and uppercase withserviceStackNameInvalid. So a ref is simply${hostname_varname}with the literal hostname (servicecache→${cache_port}); there is no dash-to-underscore rewrite to reason about, because a dashed hostname cannot exist.
Only legacy envIsolation=none auto-injects every sibling's vars as bare <host>_KEY OS env vars without a ref — see Isolation Modes. New projects are service; rely on explicit refs.
Cross-Service References in API vs Runtime
Cross-service references (${hostname_varname}) are resolved at container start time, not at definition time:
zerops_discoverwithincludeEnvs=truereturns the literal template (e.g.,${db_password}), NOT the resolved value. The service-env API stores templates, not resolved values, and returns only the service's own user-set + intrinsic vars — yaml-bakedrun.envVariablescome from the app-version, project vars from the project scope. Assemble across scopes (or read in-container) for the effective env.- Inside the running container, environment variables contain the actual resolved values.
- Restarting a service does NOT change what
zerops_discoverreturns — it always shows templates. To verify resolved values, check from inside the container.
Isolation Modes (envIsolation)
envIsolation is a project-scope setting that controls whether sibling-service vars are auto-injected.
| Mode | Behavior |
|---|---|
service (default) | Siblings are isolated. A service sees only its own vars + project vars + the explicit ${hostname_varname} refs it declares in run.envVariables. Managed-service connection vars also require an explicit ref. |
none (legacy) | Every service's vars are auto-injected into every other container as bare <host>_KEY OS env vars (source-side, directional). Ambiguous and broad — avoid for new projects. |
Set in import.yml at project or service level:
Default (service) is the right choice. Wire cross-service explicitly with ${hostname_varname} — it works in both modes, so code stays correct if isolation ever changes.
Project Variables -- Auto-Inherited
Project variables are automatically available in every service, in both runtime AND build containers — injected as OS env vars at container start. From application code, read them directly (process.env.API_URL). From zerops.yaml, reference them by name with ${VAR_NAME} — no RUNTIME_ prefix (that prefix is only for lifting a service's own runtime var into its build context).
In shell commands (buildCommands, initCommands, start) project vars are directly readable:
In build.envVariables / run.envVariables YAML (to forward a project var under a framework-conventional name) reference it directly without prefix:
To override a project variable for one service, define a service-level variable with the same key and a DIFFERENT VALUE (not a reference to the project var):
Self-Shadow Trap — same name on both sides
Writing varname: ${varname} (the left key identical to the source on the right) is always wrong:
The interpolator sees the service-level variable of that name first, can't recurse back to the inherited/referenced value, and the OS env var resolves to the literal string ${varname} — the app then connects to "${db_hostname}:5432" and crashes.
- For a project var you only want to read: delete the line — it already auto-inherits.
- For a cross-service value: use a different left-hand name (
DB_HOST: ${db_hostname}), never the same name.
Typical pattern: project-level URL constants for dual-runtime recipes
Dual-runtime recipes (frontend SPA + backend API) use project-level URL constants as the single source of truth for cross-service URLs, derived from ${zeropsSubdomainHost} (a platform-generated project-scope var present from project creation):
The frontend consumes API_URL via plain ${API_URL} in build.envVariables (baked into the bundle at compile time). The API consumes FRONTEND_URL via plain ${FRONTEND_URL} in run.envVariables (CORS allow-list). Set the same names on the workspace project via zerops_env project=true action=set after provision.
Secret Variables
- Defined via GUI, import.yml
envSecrets, ordotEnvSecrets - Read is privilege-gated -- masked in GUI; via API an admin/write token returns the value verbatim, a read-only token returns
REDACTED(keyed onsensitive=true). In-container the value is plaintext (the app needs it). Project-levelsensitive=truedoes NOT persist — only service-level is a true secret surface. - Can be updated without redeploy, but the service must be restarted to pick it up.
- A yaml-baked
run.envVariableskey and a secret on the same key cannot coexist — the platform rejects the secret withuserDataDuplicateKey. The yaml owns the key; edit the yaml and redeploy to change it.
dotEnvSecrets
Import secrets in .env format within import.yml:
All entries become secret variables. Requires #zeropsPreprocessor=on if using generator functions.
envReplace -- File-Level Substitution
Replaces placeholders in deployed files with environment variable values during deployment (not at runtime).
| Parameter | Required | Description |
|---|---|---|
delimiter | Yes | Wrapping characters (e.g., %% makes %%VAR%%). String or array |
target | Yes | Files or directories to process. String or array |
DO NOT expect directory targets to recurse into subdirectories. ./config/ processes only files directly in config/, not config/jwt/. Specify each subdirectory explicitly.
Naming Restrictions
Key: must match [a-zA-Z_]+[a-zA-Z0-9_]*. Case-sensitive. Must be unique within scope regardless of case.
Value: ASCII only. No EOL characters.
Restart Requirement
An env-store change (secret or project) propagates to the container in ~5–10s without a redeploy, but the running process keeps its boot-time environ — only newly-spawned processes see it.
DO NOT expect hot-reload of env vars. After changing secrets or project vars, restart the service (not reload — reload does not re-read env for the running process; PHP-FPM keeps its boot config). For zerops.yml run.envVariables changes, a full redeploy is required (they are baked into the app version).
System-Generated Variables
Zerops auto-generates variables per service (e.g., hostname, PATH, DB connection strings). Some are hard-reserved and rejected if you try to set them — PATH (uppercase) returns userDataUseOfSystemKey in any envVariables block. Overridable platform vars include envIsolation / sshIsolation / zeropsSubdomainHost (and the CDN URLs) — but never PATH. Reference any of them from another service with an explicit ${hostname_varname}.