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. Both project-level vars AND cross-service vars (${hostname_varname}) auto-inject as OS env vars into every container in the project — no declaration required. run.envVariables exists only for mode flags and framework-convention renames. Re-declaring an auto-injected var under its own name creates a literal-string self-shadow. Secret vars are write-only after creation. Changes require service restart.
Scope Hierarchy
| Scope | Defined In | Visibility | Editable Without Redeploy |
|---|---|---|---|
| Project | import.yml project.envVariables, GUI | All services (auto-inherited) | Yes (restart required) |
| Service secret | import.yml envSecrets, GUI | Single service | Yes (restart required) |
| 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
When the same key exists at multiple levels:
- Service basic (build/runtime) wins over service secret
- Service-level wins over project-level
- Build and runtime are separate environments -- same key can have different values in each
DO NOT create a secret and a basic runtime variable with the same key expecting both to persist. The basic runtime variable from zerops.yml silently overrides the secret.
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 | Use prefix |
|---|---|---|
Runtime var API_KEY | Build container | ${RUNTIME_API_KEY} |
Build var BUILD_ID | Runtime container | ${BUILD_BUILD_ID} |
Cross-Service References — Auto-Injected Project-Wide
Every service's variables are automatically injected as OS environment variables into every other service's containers — both runtime and build. A worker container sees db_hostname, db_password, queue_user, storage_apiUrl, etc. as real OS env vars at container start. Zero declaration in zerops.yml required.
Read them directly in application code:
run.envVariables and build.envVariables have two legitimate uses only:
-
Mode flags — per-setup values that don't come from another service:
-
Framework-convention renames — forward a platform var under a different name because the framework config expects it. The key on the left MUST DIFFER from the source var name on the right:
Do NOT re-declare auto-injected vars under their own name. It is always wrong and never useful:
The referenced variable does not need to exist at definition time — Zerops resolves at container start.
Self-Shadow Trap
Writing varname: ${varname} in run.envVariables creates a literal-string self-shadow. The platform's interpolator sees the service-level variable of that name first, can't recurse back to the auto-injected value, and the resolved OS env var becomes the literal string ${varname}:
At runtime, the worker tries to connect to "${db_hostname}:5432" and crashes. The fix is to delete the entire block — those vars are already in the container's env without any declaration.
This applies identically to project-level vars (${STAGE_API_URL}, ${APP_SECRET}) and cross-service vars (${db_hostname}, ${queue_user}) — both auto-propagate, both self-shadow under the same rule.
Hostname transformation: dashes become underscores. Service my-db variable port is ${my_db_port}.
Cross-Service References in API vs Runtime
Cross-service references (${hostname_varname}) are resolved at container start time, not at definition time. This means:
zerops_discoverwithincludeEnvs=truereturns the literal template (e.g.,${db_password}), NOT the resolved value. This is expected — the API stores templates, not resolved values.- 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 (e.g., via SSH or application endpoint).
Isolation Modes (envIsolation)
envIsolation does NOT control whether cross-service vars auto-inject — they do, in every mode. It controls something narrower: how ${hostname_varname} templates inside zerops.yml and import.yml resolve during platform interpolation.
| Mode | Behavior |
|---|---|
service (default) | Service-scoped: ${hostname_varname} templates inside that service's YAML resolve by following the hostname prefix. The OS env in every container still contains every other service's vars as auto-injected keys. |
none (legacy) | Cross-service references can be written without the ${hostname_varname} prefix (e.g. ${password} resolves to the nearest match). Do not use for new projects — ambiguous, error-prone. |
Set in import.yml at project or service level:
Default (service) is the right choice. The auto-inject behavior above applies under the default.
Project Variables -- Auto-Inherited
Project variables are automatically available in every service, in both runtime AND build containers. The platform injects them as OS env vars at container start in every service's runtime container and also in every service's build container during the build phase. From zerops.yaml's point of view they are referenced directly by name with ${VAR_NAME} — no RUNTIME_ prefix in either scope. The RUNTIME_ prefix is reserved for a different use case: lifting a single service's service-level runtime variable into that same service's build context. Project-scope vars are broader than service-scope and do not need lifting.
In shell commands (buildCommands, initCommands, start) project vars are directly readable:
In build.envVariables YAML (to compose a derived var that the bundler consumes) reference the project var directly without prefix:
In run.envVariables YAML (to forward a project var under a framework-conventional name without creating a shadow), reference directly without prefix:
DO NOT re-reference an auto-injected variable under its SAME name — that's a self-shadow loop. Applies to BOTH project-level vars AND cross-service vars:
All four resolve to the literal string ${VAR_NAME} inside the container — the framework tries to connect to "${db_hostname}:5432" and crashes. The fix is to delete those lines entirely — the platform already injects the real value as an OS env var.
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):
Typical pattern: project-level URL constants for dual-runtime recipes
Dual-runtime recipes (frontend SPA + backend API on the same platform) use project-level URL constants as the single source of truth for cross-service URLs. The constants are derived from ${zeropsSubdomainHost} (a platform-generated project-scope env var present from project creation) and the services' known hostnames:
The platform resolves ${zeropsSubdomainHost} when injecting the value into services at container start. The frontend consumes STAGE_API_URL via plain ${STAGE_API_URL} in build.envVariables (baking it into the bundle at compile time) — no RUNTIME_ prefix. The API consumes STAGE_FRONTEND_URL via plain ${STAGE_FRONTEND_URL} in run.envVariables (for CORS allow-list). The same names must be set on the workspace project via zerops_env project=true action=set after provision, so workspace verification doesn't see literal ${STAGE_FRONTEND_URL} strings.
Secret Variables
- Defined via GUI, import.yml
envSecrets, ordotEnvSecrets - Write-only after creation -- values masked in GUI, cannot be read back via API
- Can be updated without redeploy, but service must be restarted
- Overridden by basic (zerops.yml) variables with the same key
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
Env var changes (secret or project) take effect only on container start. The running process does not receive updated values.
DO NOT expect hot-reload of env vars. After changing secrets or project vars in GUI, restart the service. For zerops.yml envVariables changes, a full redeploy is required.
System-Generated Variables
Zerops auto-generates variables per service (e.g., hostname, PATH, DB connection strings). Cannot be deleted. Some read-only (hostname), others editable (PATH). Can be referenced by other services using ${hostname_varname}.
Common Mistakes
- DO NOT re-reference auto-injected vars under their own name — self-shadow loop. Applies to BOTH project-level (
STAGE_API_URL: ${STAGE_API_URL}) AND cross-service (db_hostname: ${db_hostname},queue_user: ${queue_user}). - DO NOT declare cross-service vars you only want to READ — they are already in the container's OS env. Read via
process.env.db_hostname/getenv('db_hostname')directly. Declare inrun.envVariablesonly to RENAME (e.g.DB_HOST: ${db_hostname}) or to set mode flags. - DO NOT forget restart after GUI/API env changes — process won't see new values
- DO NOT expect
envReplaceto recurse subdirectories — it does not - DO NOT rely on reading secret values back — they are write-only after creation
- DO NOT create both secret and basic vars with same key — basic silently wins