Skip to main content
Skip to main content

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

ScopeDefined InVisibilityEditable Without Redeploy
Projectimport.yml project.envVariables, GUIAll services (auto-inherited)Yes (restart to apply)
Service secretimport.yml envSecrets, GUISingle serviceYes (restart to apply)
Service basic (build)zerops.yml build.envVariablesBuild container onlyNo (redeploy required)
Service basic (runtime)zerops.yml run.envVariablesRuntime container onlyNo (redeploy required)

Variable Precedence

Total order for the bare key (highest wins): system/platform > yaml-baked run.envVariables > service secret/userData > project.

  1. yaml-baked run.envVariables owns its key — a service secret/userData set on the same key is rejected (userDataDuplicateKey); the two never coexist. To change a yaml-baked value, edit zerops.yml and redeploy.
  2. Service-level wins over project-level.
  3. 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 accessFromHow
Runtime var API_KEYBuild container${RUNTIME_API_KEY} — runtime run.envVariables are known at build time, so the build can read them
Build var BUILD_IDRuntime containerNot 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.
zerops:
- setup: app
build:
envVariables:
API_KEY: ${RUNTIME_API_KEY} # reads runtime API_KEY during build
run:
envVariables:
API_KEY: "12345-abcde"

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:

run:
envVariables:
DB_HOST: ${db_hostname}
DB_PASS: ${db_password}
DATABASE_URL: ${db_connectionString}
CACHE_URL: ${cache_connectionString}
  • 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 with serviceStackNameInvalid. So a ref is simply ${hostname_varname} with the literal hostname (service cache${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_discover with includeEnvs=true returns 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-baked run.envVariables come 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_discover returns — 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.

ModeBehavior
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:

project:
envIsolation: none # legacy — avoid; default is service
services:
- hostname: db
envIsolation: none # per-service: expose THIS service's vars to siblings

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:

build:
buildCommands:
- echo "building for $API_URL" # shell reads the OS env var
- VITE_API_URL=$API_URL npm run build # or pass it forward by shell prefix

In build.envVariables / run.envVariables YAML (to forward a project var under a framework-conventional name) reference it directly without prefix:

build:
envVariables:
VITE_API_URL: ${API_URL} # project var API_URL read as-is
run:
envVariables:
CORS_ALLOWED_ORIGIN: ${FRONTEND_URL} # forwarded under a different name

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

run:
envVariables:
LOG_LEVEL: debug # overrides project-level LOG_LEVEL for this service

Self-Shadow Trap — same name on both sides

Writing varname: ${varname} (the left key identical to the source on the right) is always wrong:

run:
envVariables:
API_URL: ${API_URL} # project-level self-shadow
db_hostname: ${db_hostname} # cross-service self-shadow

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

project:
envVariables:
API_URL: https://apistage-${zeropsSubdomainHost}-3000.prg1.zerops.app
FRONTEND_URL: https://appstage-${zeropsSubdomainHost}.prg1.zerops.app

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, or dotEnvSecrets
  • Read is privilege-gated -- masked in GUI; via API an admin/write token returns the value verbatim, a read-only token returns REDACTED (keyed on sensitive=true). In-container the value is plaintext (the app needs it). Project-level sensitive=true does 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.envVariables key and a secret on the same key cannot coexist — the platform rejects the secret with userDataDuplicateKey. The yaml owns the key; edit the yaml and redeploy to change it.

dotEnvSecrets

Import secrets in .env format within import.yml:

services:
- hostname: app
dotEnvSecrets: |
APP_KEY=generated_value
DB_PASSWORD=secure123

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

run:
envReplace:
delimiter: "%%"
target:
- ./config/
- ./templates/settings.json
ParameterRequiredDescription
delimiterYesWrapping characters (e.g., %% makes %%VAR%%). String or array
targetYesFiles 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}.