Production

Machine & CI access

The env-var keystone for non-interactive Envless: token to fetch ciphertext, key or passphrase to decrypt locally.

On a laptop, envless login caches an API token at ~/.envless/config.json, and envless passphrase set caches the derived workspace key in that same file. CI runners, Docker builds, and production hosts have neither, so they configure Envless entirely through environment variables. This page is the complete keystone for that non-interactive path.

The two roles

Every non-interactive setup needs to answer two separate questions, and they use two separate credentials:

Fetch the ciphertext

ENVLESS_TOKEN is an ev_sk_ API key. It authenticates the request that pulls your encrypted bundle from the API. The server only ever hands back ciphertext, so the token alone can never read a value.

Decrypt it locally

ENVLESS_KEY (or ENVLESS_PASSPHRASE) is the material that turns ciphertext into plaintext on the machine. It never leaves the runner and is never sent to the API.

This split is the whole point of the zero-knowledge model: whoever holds the token can trigger a fetch, but without the key or passphrase they get nothing but encrypted blobs. Keep the two in separate secrets so you can rotate them independently.

Every variable at a glance

VariableRequiredWhat it doesWhere to get it
ENVLESS_TOKEN✅ev_sk_ API key that authenticates the ciphertext fetchDashboard → Keys
ENVLESS_KEYone of thesebase64url of the raw AES-256 workspace key, used to decrypt locally with no KDFDashboard → Settings → CI / Machine Access
ENVLESS_PASSPHRASEone of theseWorkspace passphrase, from which the key is derived via PBKDF2The workspace passphrase, the one envless passphrase set prompts for
ENVLESS_PRODUCT⬜ (or .envless)Product slug, when the project slug is ambiguousDashboard / your .envless file
ENVLESS_PROJECT✅ (or .envless)Project slug to fetchDashboard / your .envless file
ENVLESS_ENV✅ (or .envless)Environment slug to fetchDashboard / your .envless file
ENVLESS_NO_UPDATE_CHECKoptionalCLI only. Skips the check that prints the Update available! boxn/a
ENVLESS_NO_INSECURE_CACHE_WARNINGoptionalCLI only. Silences the warning envless passphrase set prints about the key it caches in ~/.envless/config.jsonn/a

Both switches read the same four values: 1, true, yes, on, matched after trimming and lower-casing. Anything else leaves the switch off, so ENVLESS_NO_UPDATE_CHECK=0 and ENVLESS_NO_UPDATE_CHECK=false still run the check. Setting a switch to the empty string leaves it off too.

Provide one decryption credential: ENVLESS_KEY or ENVLESS_PASSPHRASE. If both are set, ENVLESS_KEY wins and the passphrase is ignored. Prefer ENVLESS_KEY: it's the raw key, so there's no per-boot PBKDF2 cost and no passphrase sitting in your CI secret store.

Where each credential comes from

ENVLESS_TOKEN: the Keys page

Open envless.cloud/dashboard/keys, select the workspace, and create a key with a descriptive name (ci-production, github-actions). The full ev_sk_... value is shown once on creation and stored hashed. Copy it straight into your secret store. Rotating or deleting it there cuts access instantly, and both ask you for a code emailed to you first. See Keys for naming and rotation.

ENVLESS_KEY: the CI / Machine Access page

Open Settings → CI / Machine Access. This exports the workspace key as base64url(raw AES-256 key). Paste that into ENVLESS_KEY. Because it's the raw key, the runner skips key derivation entirely and never needs your passphrase.

ENVLESS_PASSPHRASE: the alternative

If you'd rather not export the raw key, set ENVLESS_PASSPHRASE to the workspace passphrase instead. Envless derives the same AES-256 key from it on the runner (PBKDF2, 200,000 iterations, SHA-256). Functionally identical output, slower cold start: this path also spends one extra API call resolving the workspace id that the salt is built from.

Selecting project and environment

The token identifies the workspace, but you still have to say which project and environment to pull. Envless resolves each field in this order:

  1. The --product / --project / --env flag (CLI only).
  2. The ENVLESS_PRODUCT / ENVLESS_PROJECT / ENVLESS_ENV environment variables.
  3. The committed .envless file at the project root.

They all take a slug (e.g. billing-api, production). In CI, set them explicitly rather than relying on the file. That's unambiguous and survives a checkout that omits .envless.

bash
export ENVLESS_PROJECT=billing-api
export ENVLESS_ENV=production

What actually happens on the runner

Fetch

Using ENVLESS_TOKEN, Envless requests the encrypted bundle for ENVLESS_PROJECT / ENVLESS_ENV. Only ciphertext crosses the wire. @goenvless/env skips that request when it already holds a bundle under ~/.envless/cache less than 15 minutes old, unless you call load({ revalidate: true }). sync and run always refetch.

Decrypt

ENVLESS_KEY is used directly, or the key is derived from ENVLESS_PASSPHRASE. Decryption is entirely local.

Populate

Decrypted values are written into process.env (via @goenvless/env/register or an awaited load()) or handed to the child process (run). The loader leaves alone any name already set in the ambient environment. run does the opposite, and the decrypted value wins inside the child it spawns.

Refuse

A name that decides how a process executes code, NODE_OPTIONS, PATH, LD_PRELOAD, DYLD_INSERT_LIBRARIES and the rest of that family, is decrypted but never written into process.env and never handed to the child, because storing one turns write access to a variable into code execution. Envless prints the list it refused. load() still returns those values, so code that genuinely wants one can read it deliberately.

GitHub Actions

Store ENVLESS_TOKEN and ENVLESS_KEY as repository or environment secrets, then choose the consumption mode that fits the step.

sync and run are commands of the envless binary, which ships in @goenvless/cli. A runner starts with nothing installed, so every job that calls them needs an install step first. The /register mode calls no binary at all: it needs @goenvless/env in your package.json, which npm ci restores like any other dependency.

The examples install the CLI globally. To pin its version in your lockfile instead, add @goenvless/cli to devDependencies and call it as npx envless sync. npx resolves the binary from node_modules/.bin, which only holds it when the package is a dependency of the project.

name: deploy
jobs:
    deploy:
        runs-on: ubuntu-latest
        env:
            ENVLESS_TOKEN: ${{ secrets.ENVLESS_TOKEN }}
            ENVLESS_KEY: ${{ secrets.ENVLESS_KEY }}
            ENVLESS_PROJECT: billing-api
            ENVLESS_ENV: production
        steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
            - run: npm ci
            # puts the envless binary on PATH for the steps below
            - run: npm i -g @goenvless/cli@latest
            # writes .env.production, decrypted, mode 0600
            - run: envless sync
            - run: npm run deploy

ENVLESS_NO_UPDATE_CHECK: 'true' is worth adding to any CI job that shells out to the envless CLI. It drops the version-check network call and keeps logs clean. A job that only imports @goenvless/env never makes that call in the first place.

Docker

Mount the token and key as build secrets so they never land in an image layer, then run the container with the @goenvless/env/register hook.

Dockerfile
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ENV ENVLESS_PROJECT=billing-api ENVLESS_ENV=production
# NODE_OPTIONS carries the preload into the build script, so it starts with the
# variables already loaded. Without it the secrets below are mounted for nothing.
RUN --mount=type=secret,id=envless_token \
    --mount=type=secret,id=envless_key \
    ENVLESS_TOKEN=$(cat /run/secrets/envless_token) \
    ENVLESS_KEY=$(cat /run/secrets/envless_key) \
    NODE_OPTIONS='--import @goenvless/env/register' \
    npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=build /app .
# ENVLESS_TOKEN + ENVLESS_KEY provided at runtime by the platform
ENV ENVLESS_PROJECT=billing-api ENVLESS_ENV=production
CMD ["node", "--import", "@goenvless/env/register", "server.js"]
build
docker build \
    --secret id=envless_token,env=ENVLESS_TOKEN \
    --secret id=envless_key,env=ENVLESS_KEY \
    -t myapp .

For the running container (Fly.io, Railway, Render, ECS, Kubernetes), set ENVLESS_TOKEN and ENVLESS_KEY as platform secrets and ENVLESS_PROJECT / ENVLESS_ENV as plain env vars. The /register loader fetches and decrypts at boot.

Rotation

Because the token and key are independent, rotate whichever leaked without disturbing the other:

  • Token compromised → rotate it on the Keys page, which mints a fresh ev_sk_ and stops the old one authenticating, then update the CI secret. It only ever saw ciphertext, so no plaintext value leaked, but assume everything its scopes allowed it to write was reachable.
  • Key/passphrase compromised → rotate the workspace passphrase from Settings → Encryption in the dashboard. That decrypts every variable in the workspace and re-encrypts it under the new passphrase, so the leaked key opens nothing afterwards. Re-export a fresh ENVLESS_KEY from the CI / Machine Access page and update every runner.