opsdocsConsole

Packages / ops-env

Loading

What load() reads in production and on your machine, which value wins, and how errors look.

Updated 13 Oct 2026View as Markdown
situationwhat load() does
ENV_SECRET is set (a deployed service)reads /run/secrets/$ENV_SECRET. A missing file, a directory, or a file that does not parse throws, and nothing from it is set
ENV_SECRET is unset or empty (your machine, tests)reads .env in the working directory if there is one. A missing file is fine; a broken one prints a warning and sets nothing

SECRETS_DIR moves the directory the secret is read from, for a setup that mounts secrets somewhere other than /run/secrets. Parent directories are never searched for .env.

What wins

A variable already in the environment wins, even when it is empty. Within the file, the first of a repeated name wins. That is how a plain container variable, such as the DATABASE_URL of a SQLite database the service mounts, overrides a group key of the same name.

A failed load stops the process

Under --import or --preload, a thrown error stops Node or Bun before your app starts. That is the point: a service that boots with half its configuration silently absent is worse than one that does not boot.

text
EnvFileError: ENV_SECRET names 'web-env' but /run/secrets/web-env is not there: the deploy did not render this service's env secret

Errors name the file and the line, never the line's content, because that content is a secret and the error lands in the boot log.

For tests

ts
import { loadFrom } from '@syncronet/ops-env'

const env: Record<string, string | undefined> = {}
loadFrom('web-env', '/tmp/fixtures', env, process.cwd())

loadFrom(secretName, secretsDir, env, cwd) does what load() does into an object you pass. parse(text, source) returns the [name, value] pairs of a file and throws EnvParseError on a broken one.