Deployments · Environment variables
Environment variables
Pass configuration into your service and its build. Plain values for connection strings and log levels, encrypted secret variables for tokens and credentials, and a scope on each one that decides whether the build, the running service, or both can read it.
Two kinds of variables
| Kind | Use for | Where it's stored | Visible in logs / errors | Default scope |
|---|---|---|---|---|
| Plain | Log level, region, feature flags, public URLs | Service config | Yes | Build & runtime |
| Secret | API tokens, DB passwords, signing keys | Encrypted at rest | Never | Runtime only |
Both render at runtime via the standard process.env (Node), os.environ (Python), os.Getenv (Go), $ENV{} (Perl), etc. Your code can't tell which is which — it's just DATABASE_URL. The scope decides whether the build sees a variable too.
Set them
Set plain variables on the service. Subsequent deployments inherit them.
$ csph service variables set LOG_LEVEL=info REGION=us-eastSet secret variables the same way — they're stored encrypted and never logged. The command sets the whole secret set: on a service that already has secrets, name the others with a blank value to keep them (see Rolling restart on secret change).
$ csph service secrets set DATABASE_URL=postgres://... STRIPE_KEY=sk_live_...List what's currently set:
$ csph service variables list --service svc_abc --environment env_abcRemove one:
$ csph service variables unset LOG_LEVELThe CLI sets values only. Every key keeps its default scope (plain variables Build & runtime, secrets Runtime only). To change a scope, use the console or the API.
Build, runtime, or both
Every variable and secret on a service has a scope. It decides where the value is delivered: into the build that produces your image, into the running service, or both.
| Scope (console) | API value | The build can read it | The running service can read it | Default for |
|---|---|---|---|---|
| Build & runtime | both | Yes | Yes | Plain variables |
| Runtime only | runtime | No | Yes | Secrets |
| Build only | build | Yes | No | — |
- Secrets stay out of builds unless you opt in. A secret reaches the build only when you set it to Build & runtime or Build only. The console warns when you do: "The build can read this secret. It isn't stored in the image, but anything your build writes into its output is."
- Build only keeps a value out of the running service. It isn't set in the running spherelets, the service's cron runs, or its pre-deploy command. Use it for values only the build needs — a private package registry token, a source-map upload key.
- Browser-bundle values are public. Frameworks inline some variables into the JavaScript they ship to the browser. When a plain variable whose name starts with
NEXT_PUBLIC_,VITE_,REACT_APP_,PUBLIC_,NUXT_PUBLIC_,EXPO_PUBLIC_, orGATSBY_is in the build's scope, the console marks it "Included in the browser bundle — visible to anyone." Never put a credential in one.
How the build receives them
Delivery depends on who writes the Dockerfile.
| Build type | Plain variables in scope | Secrets opted into the build | Written into the image? |
|---|---|---|---|
| From source, no Dockerfile (native runtimes and static sites) | Environment variables of your build command, any name | Environment variables of your build command | No — the values are handed to the builder as a build secret, not stored in image layers or history |
| From your Dockerfile | Build args — only take effect where your Dockerfile declares ARG NAME | Build secrets, read with RUN --mount=type=secret,id=NAME from /run/secrets/NAME | Build-arg values can be recorded in the image's history. Build secrets are not. |
Builds without a Dockerfile
Nothing to configure. Every variable in the build's scope is exported into the shell that runs your build command, whatever its prefix — process.env.SENTRY_AUTH_TOKEN in a Node build script works the same as NEXT_PUBLIC_API_URL.
Caching still works: changing a build-scoped value re-runs the build step, while a separate dependency install step stays cached. That install step runs before the variables are loaded, so it can't read them. If installing dependencies needs a token (a private npm registry, for example), put the install inside your build command — npm ci && npm run build — rather than in a separate install command.
Builds from your Dockerfile
Plain variables arrive as build args. A build arg only takes effect in a stage that declares it, so add ARG NAME to every stage that uses the value. Secrets you opt into the build arrive as build secrets, one per key, mounted as a file at /run/secrets/<KEY> for the single RUN step that asks for it.
FROM node:20-slim AS build
WORKDIR /app
# Plain variable in the build's scope — declare it to use it.
ARG NEXT_PUBLIC_API_URL
COPY package.json package-lock.json .npmrc ./
# Secret opted into the build — readable only by this step, never stored in the image.
RUN --mount=type=secret,id=NPM_TOKEN \
NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) npm ci
COPY . .
RUN npm run build
- Don't put secrets in plain variables. Build-arg values that a
RUNstep uses are recorded in the image's history, where anyone who can pull the image can read them. Store credentials as secrets and use the mount. - Only the file form is supported. The
env=option on a secret mount isn't available; read the file at/run/secrets/<KEY>. - Explicit
build_argswin. Abuild_argsentry incomputesphere.yamloverrides a variable of the same name. Abuild_argskey can't share a name with a secret.
Applying a change
A build-scoped value is read when the image is built, so a redeploy of the existing image can't change it. The console picks the right action when you save.
| What you changed | Button in the Apply changes? dialog | What happens |
|---|---|---|
| A Build only variable, or a browser-bundle variable that's in the build's scope | Save & rebuild | Saves, then starts a new build that reads the new value and deploys it. |
| Anything else | Save & redeploy | Saves, then restarts the service on the existing image with the new values. A changed Build & runtime value reaches the build the next time the service builds. |
| Anything | Save only | Saves without touching the running service. The next deploy or build picks it up. |
Save & rebuild appears only on services that build from a Git repository. A service running your own prebuilt image has no build to re-run.
From the API
GET /v2/deployments/{id}/env-vars returns variable_scopes with the effective scope of every key, defaults filled in. Set the scope of one plain variable with the single-key endpoint:
curl --request PUT \
--url "https://api.computesphere.com/v2/deployments/DEPLOYMENT_ID/env-vars/SENTRY_AUTH_TOKEN" \
--header "Authorization: Bearer YOUR_API_TOKEN" \
--header "Content-Type: application/json" \
--data '{ "value": "sntrys_…", "scope": "build" }'
Secrets and bulk changes go through PUT /v2/deployments/{id}/env-vars with a variable_scopes map. That map replaces the saved scopes: any key you leave out goes back to its default, so send the scope of every key you've changed from its default. Omit variable_scopes entirely to keep the saved scopes. Add "skip_redeploy": true to save without redeploying.
Change one secret without losing the others
PUT /v2/deployments/{id}/env-vars replaces each map you send. A key you leave out of secret_vars is deleted, so sending only the secret you're changing removes every other secret on the service:
{ "secret_vars": { "STRIPE_KEY": "sk_live_new" } }
That body leaves STRIPE_KEY as the service's only secret.
Secret values come back blank when you read them, and a blank value keeps the saved one. To change one secret, send every secret, giving the ones you aren't changing a blank value:
curl --request PUT \
--url "https://api.computesphere.com/v2/deployments/DEPLOYMENT_ID/env-vars" \
--header "Authorization: Bearer YOUR_API_TOKEN" \
--header "Content-Type: application/json" \
--data '{ "secret_vars": { "DATABASE_PASSWORD": "", "SENTRY_DSN": "", "STRIPE_KEY": "sk_live_new" } }'
Leave env_vars out of the body to keep your plain variables as they are; send an empty object only to clear them.
Share variables across an environment
The variables above live on a single service. You can also set variables and secrets at the environment level — every service deployed into that environment inherits them. Use it for the config a whole environment shares: a log level, a region, a datastore connection string.
| Where you set it | Who gets it |
|---|---|
| Service | Just that service's deployments |
| Environment | Every service deployed into the environment |
A service variable always wins. If a service defines a variable with the same name as an environment one, the service's value is used for that service — the environment value is the default, the service value is the override. A service that doesn't define the key inherits the environment value.
$ csph environment variables set LOG_LEVEL=info REGION=us-east --environment env_abc$ csph environment secrets set DATABASE_URL=postgres://... --environment env_abc$ csph environment variables list --environment env_abcChanging an environment variable or secret rolls the inheriting services so they pick up the new value — the same restart experience as updating a service's own.
Environment-level variables and secrets are runtime only. They don't have a scope and never reach a build. If a build needs a value, set it on the service.
In computesphere.yaml
Declare environment variables and secrets right in the manifest so a single
csph apply provisions them. Each environments block binds to a real
environment by name, so its values land only there.
version: "2"
project:
name: myapp
environments:
- name: production
region: us-east
variables: # plain values, or pulled from another service
LOG_LEVEL: info
DATABASE_URL:
fromService: db
property: connectionString
secrets: # placeholder or fromService — never a plaintext value
STRIPE_KEY:
secret: true
services:
- name: api
type: web-service
image: myregistry/api:v1.0
port: 8000
env_vars:
LOG_LEVEL: debug # overrides the environment value for this service
- name: db
type: postgresql
A few rules keep secrets safe and re-apply predictable:
- Secrets never hold a plaintext value in the file. Use
secret: trueto declare the key and set the value out of band (csph environment secrets setor the console), orfromServiceto pull it from a service you're deploying (like a datastore connection string). - Re-applying never wipes a value you set elsewhere. A
secret: trueplaceholder only ensures the key exists — if the value was already set, apply leaves it untouched. fromServiceresolves at apply time from the named service's output, so the connection string is never typed or committed.
Rolling restart on secret change
When you update a secret variable, the running spherelets get a rolling restart so the new value takes effect — without redeploying the image. Plain-variable changes apply on the next deployment.
Rotating a credential restarts the service with the new value, and the deployment lifecycle's zero-downtime rules apply: old spherelets keep serving until new ones pass health checks. See the lifecycle for the full sequence.
csph service secrets set sets the service's whole secret set, and secret values can't be read back. To change one secret on a service that has others, name each of the others with a blank value, which keeps its saved value:
$ csph service secrets set DATABASE_PASSWORD= SENTRY_DSN= STRIPE_KEY=sk_live_newcsph service secrets list shows the names to include. The CLI refuses a set that leaves out an existing secret rather than drop it. --replace sets exactly the secrets you list and deletes the rest. From the API, use the same blank-value form shown in Change one secret without losing the others.
What gets injected
Nothing, today. A spherelet starts with exactly the variables and secrets you set, at the environment and service level, and no others.
In particular there is no PORT variable. Your service must listen on the port you configured for it (the service's port setting). The health check and the public route use that port. If your app reads PORT, set a variable named PORT to the same number yourself.
Limits
- Name: alphanumeric and underscores, must start with a letter. Max 64 characters.
- Value: max 32 KB per variable. Bigger payloads belong in SphereStor or a managed datastore.
- Total: 200 variables per service combined (plain + secret).
Honest limits
- Scopes are set in the console or the API only.
computesphere.yamland thecsphCLI don't accept a scope yet; variables they create take the default for their kind. - Environment-level variables can't be scoped. They're runtime only. Put build-time values on the service.
- A secret in the build is only as safe as the build. The platform keeps opted-in secrets out of image layers and history, but if your build command prints the value or writes it into a file that ends up in the image or the published site, it's exposed. Opt a secret in only when the build needs it.
- The dependency install step can't read variables in builds without a Dockerfile. Run the install inside your build command when it needs one.
- Promoted images keep their build-time values. An image promoted from another environment was built with that environment's build-scoped values. Rebuild in the target environment when those have to differ.
Next
- Pre-deploy commands — run migrations against the new image before it takes traffic
- Promote — ship the exact image you tested in one environment to another
- Health checks — what the rollout uses to decide your new spherelets are ready
- Lifecycle — what happens to your variables on each deploy, restart, and rollback