Skip to content
RuntimeTypeScript

Bun environment variables: .env loading and Bun.env

Bun loads up to four .env files on its own, lets shell variables win over all of them, and can inline values into bundles. What that means for your secrets.

BunAt a glance

Env file that wins
.env.$(NODE_ENV).local

First of 4 .env files, highest priority first. See the load order

Public prefix
None

No names are copied into browser code by a prefix.

Values applied
At runtime

Values are read from the environment when the app runs.

Built-in secret store
None: process.env, with .env files loaded automatically

bun.com/docs/runtime/environment-variables

Crunu command
crunu run --env api/development -- bun --watch src/index.tsRecipeCrunu has no dedicated integration here: this is the general setup.

Common pitfalls

  1. Bun loads .env files by itself for scripts, tests and even bun install, so a forgotten .env.local can change what any command sees.
  2. Shell variables beat every .env file, and Bun has no option to reverse that order.
  3. bun build --env inline copies every process.env value referenced in your code into the bundle. A prefix such as PUBLIC_* is safer.
  4. When Bun runs as node, for example with --bun, it stops loading .env files automatically to match Node.js.
  5. .env.local is skipped when NODE_ENV is test, so test runs read .env.test and .env only.
On this page

Bun reads environment variables from the process environment and, unlike Node.js, also loads .env files on its own every time it starts: .env, then the file for the current NODE_ENV, then .env.local, then the NODE_ENV-specific .local file, with later files winning. Variables already set in the shell win over all of them. The values are available as process.env, Bun.env and import.meta.env, which are the same object. The rules here follow the Bun documentation on environment variables for Bun 1.4, checked October 30, 2026.

Which files Bun reads

Bun lists its files in order of increasing precedence. Read from strongest to weakest:

PrecedenceFileNotes
1.env.production.local, .env.development.local or .env.test.localchosen by NODE_ENV
2.env.localnot loaded when NODE_ENV=test
3.env.production, .env.development or .env.testchosen by NODE_ENV
4.envalways

Above all four sits the shell. If DATABASE_URL is exported in your terminal, the value in .env.local is ignored. Bun's documentation lists only the order of the files, but this order is how Bun behaves today. A request to let files override shell variables is open as a feature request, Bun issue 25100, and its author calls the change a breaking one.

It happens for more commands than you think

Automatic loading isn't limited to bun run. Bun applies it to scripts from package.json, to bun test, and to package manager commands such as bun install. That is convenient when a private registry token lives in .env, and surprising when a developer's .env.local points tests at a real database.

Three switches control it:

  • --env-file=FILE replaces the default list with the files you name. You can repeat it, and relative paths resolve against the directory the command runs in.
  • --no-env-file turns automatic loading off for one command. Explicit --env-file files still load.
  • env = false in bunfig.toml turns it off for the project.

When Bun runs as node, for example through bun --bun or bunx --bun, it disables automatic loading on purpose, so tools that load their own files, like Vite, see a clean environment.

Expansion and types

Bun expands $OTHER_VAR references inside .env values, so DATABASE_URL=postgres://$DB_USER@localhost/app works without dotenv-expand. Escape a dollar sign that belongs to the value as \$, which matters for generated passwords.

In TypeScript every process.env property is typed string | undefined. Bun documents interface merging on Env in the bun module to declare the names you rely on. That gives autocompletion, but it isn't validation: a declared variable can still be missing at runtime, so check required values at startup.

Bundling inlines values

bun build is where a server-side habit can become a leak. By default the bundler leaves process.env.FOO as a runtime lookup. The env option changes that:

SettingEffect
disable (the CLI default)nothing is inlined
inlineevery process.env.X reference becomes the literal value from the build environment
a prefix such as PUBLIC_*only variables starting with that prefix are inlined

For anything that runs in a browser, use the prefix form. With inline, any secret your code references, even on a code path the browser never takes, is copied into the output file. Bun's own guidance is to use a prefix for public values like URLs and client-side tokens without injecting private credentials into bundles.

Run Bun with values from Crunu

Since shell variables always win in Bun, crunu run fits without configuration: it starts Bun with the variables of a Crunu environment already in the process environment, so they take precedence over any .env* file left in the project. Nothing is written to disk.

  1. After crunu login, start the dev server with crunu run --env api/development -- bun --watch src/index.ts.
  2. Run tests with crunu run --env api/test -- bun test --no-env-file, so a local .env.test.local can't quietly mix in.
  3. Build a browser bundle in CI with exec crunu run --env web/production -- bun build ./src/client.ts --outdir dist --env 'PUBLIC_*', which inlines only the public names.
  4. On a Linux server with the agent, run crunu run --env api/production --restart on-change -- bun src/index.ts.

If you move every value into Crunu, adding --no-env-file makes the setup explicit: the program sees the Crunu environment plus the short allowlist that crunu run passes from your shell, and nothing from stray files. NODE_ENV isn't on that allowlist. Put it in the environment if your code or Bun's file selection depends on it.

--restart on-change restarts the Bun process after a value changes. Bun's own --watch restarts on code changes. They work together: one reacts to files, the other to new values.

Moving a Bun project off local .env files

Most Bun projects start with a .env and a .env.local that someone sent around once. Moving them into Crunu takes a few minutes, and the order of steps matters more than the tooling:

  • Inventory first. List the files Bun would load for each NODE_ENV and note which names appear in more than one. Those overlaps are where today's behavior depends on precedence, and you want the same result afterwards.
  • One environment per stage. Drag the files into the matching Crunu environments. Crunu recognizes env files and shows each variable as a row with the value hidden, so you can check names without exposing values on a shared screen.
  • Commit a template, not values. Replace the real files with a .env.example that lists every name, so the next person knows what the program expects.
  • Delete the old copies from laptops, from the chat thread they were sent in, and from any CI cache. Values that left the team that way should be reissued at their providers, since you can't tell who still has them.

After that, the commands in the steps above are the only way the values reach Bun, and every change happens in one place.

Bun in containers

Automatic .env loading works the same inside a container: Bun looks in the directory the command runs in. If your build copies the project directory into the image, a .env file in that directory goes along with it unless .dockerignore excludes it. Starting the container's command under crunu run with a service token keeps values out of the image entirely. Whoever stores that token for the container can then decrypt its environment, so treat it as a fallback and read the limits on the Fly.io page. See the Docker Compose page and secrets in Docker images.

Frequently asked questions

Something else? Write to support@crunu.com

Does Bun need the dotenv package?

No. Bun reads .env files automatically and expands variable references inside them, so dotenv and dotenv-expand are unnecessary.

How do I stop Bun from reading .env files?

Pass --no-env-file to the command, or set env = false in bunfig.toml. Files you pass explicitly with --env-file still load.

If a variable is in Crunu and in .env.local, which value does Bun use?

The Crunu value. crunu run puts it into the process environment before Bun starts, and Bun never overwrites a variable that is already set.

What is the difference between Bun.env and process.env?

None in practice. Bun.env and import.meta.env are aliases of process.env, so they show the same values.

Sources

Every source this page relies on, with the date we last checked it.

  1. Bun: Environment variables · checked October 30, 2026
  2. Bun: Bundler · checked October 30, 2026
  3. Bun issue 25100: let .env files override system environment variables · checked October 30, 2026

Keep your .env values in one place

Free during early access. Values are encrypted on your devices, each environment has its own key, and crunu run hands them to your app without writing a file.