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
| Variable | Required | What it does | Where to get it |
|---|---|---|---|
ENVLESS_TOKEN | ✅ | ev_sk_ API key that authenticates the ciphertext fetch | Dashboard → Keys |
ENVLESS_KEY | one of these | base64url of the raw AES-256 workspace key, used to decrypt locally with no KDF | Dashboard → Settings → CI / Machine Access |
ENVLESS_PASSPHRASE | one of these | Workspace passphrase, from which the key is derived via PBKDF2 | The workspace passphrase, the one envless passphrase set prompts for |
ENVLESS_PRODUCT | ⬜ (or .envless) | Product slug, when the project slug is ambiguous | Dashboard / your .envless file |
ENVLESS_PROJECT | ✅ (or .envless) | Project slug to fetch | Dashboard / your .envless file |
ENVLESS_ENV | ✅ (or .envless) | Environment slug to fetch | Dashboard / your .envless file |
ENVLESS_NO_UPDATE_CHECK | optional | CLI only. Skips the check that prints the Update available! box | n/a |
ENVLESS_NO_INSECURE_CACHE_WARNING | optional | CLI only. Silences the warning envless passphrase set prints about the key it caches in ~/.envless/config.json | n/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:
- The
--product/--project/--envflag (CLI only). - The
ENVLESS_PRODUCT/ENVLESS_PROJECT/ENVLESS_ENVenvironment variables. - The committed
.envlessfile 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.
export ENVLESS_PROJECT=billing-api
export ENVLESS_ENV=productionWhat 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 deployname: test
jobs:
test:
runs-on: ubuntu-latest
env:
ENVLESS_TOKEN: ${{ secrets.ENVLESS_TOKEN }}
ENVLESS_KEY: ${{ secrets.ENVLESS_KEY }}
ENVLESS_PROJECT: billing-api
ENVLESS_ENV: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm i -g @goenvless/cli@latest
# injects decrypted vars into the test process only, nothing on disk
- run: envless run -- npm testname: build
jobs:
build:
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
# @goenvless/env is a project dependency, so this installs it
- run: npm ci
# /register preloads + decrypts before your entrypoint runs
- run: node --import @goenvless/env/register build.jsENVLESS_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.
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"]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_KEYfrom the CI / Machine Access page and update every runner.