env.dev

Bun Environment Variables: Bun.env & .env Auto-Load

Bun auto-loads .env, .env.{NODE_ENV}, and .env.local with no dotenv install. Bun.env vs process.env, $VAR expansion, --env-file, and build inlining gotchas.

By env.dev Updated

Bun loads .env, .env.local, and the NODE_ENV-specific file automatically — no dotenv install, no --env-file flag, no import at the top of your entry file. It even expands $VARIABLE references inside the file, something Node's built-in loader still refuses to do. The convenience cuts both ways: auto-loading means values appear "from nowhere" when you forget the file exists, and bun build --target=bun can inline secrets straight into a compiled binary. Everything here applies to Bun 1.3 (1.3.x as of June 2026).

TL;DR

  • Bun auto-loads, in increasing precedence: .env .env.development / .env.production / .env.test (per NODE_ENV) → .env.local.
  • Bun.env, process.env, and import.meta.env are aliases of the same object — pick one for consistency, not performance.
  • Variable expansion works inside .env files (URL=https://$HOST); escape a literal dollar with \$.
  • bun test sets NODE_ENV=test and therefore loads .env.test.
  • --no-env-file disables auto-loading — use it in CI/production when only real environment variables should count.

Which .env files does Bun load automatically?

Three layers, read from the project root, later layers overriding earlier ones:

FileLoaded whenPrecedence
.envAlwaysLowest
.env.development / .env.production / .env.testMatching NODE_ENV (default development)Middle
.env.localAlways, git-ignored by conventionHighest file

Variables already present in the real environment (shell exports, Docker ENV, CI) are not overwritten by the files — the same first-wins rule that the dotenv library applies. Since bun test forces NODE_ENV=test, a .env.test file is the idiomatic place for test database URLs — and the explanation when tests mysteriously see different values than bun run.

Bun.env vs process.env vs import.meta.env — which should you use?

They are documented aliases of one another — three names for the same underlying environment. Reads and writes through any of them are visible through the others:

typescript
// All three print the same value
console.log(process.env.API_URL);
console.log(Bun.env.API_URL);
console.log(import.meta.env.API_URL);

process.env.FOO = 'bar';
console.log(Bun.env.FOO); // "bar" — same object

// Print the fully resolved environment (files + shell):
// bun --print process.env

Practical advice: write process.env. It keeps the code portable to Node.js and to every library that expects it; Bun.env buys you nothing except a hard runtime dependency. Be careful with import.meta.env — in Bun it is the live environment, but in Vite-bundled code the same expression is a statically replaced, prefix-filtered object, so the identical line means two different things in two runtimes.

How does variable expansion work in Bun?

Bun expands references to previously defined variables inside .env files — matching what dotenv-expand bolts onto Node's dotenv:

ini
# .env
HOST=api.example.com
PORT=4000
API_URL=https://$HOST:$PORT/v1     # -> https://api.example.com:4000/v1

# Escape the dollar to keep a literal value
PASSWORD=pa\$\$word                # -> pa$$word

Expansion is a Bun parser feature, not part of any .env standard — there is no spec for the format at all, and parsers disagree on exactly this kind of detail. If the same file must also work under Node, Docker Compose, or Python, check the cross-parser syntax rules before relying on $VAR references.

How do --env-file and --no-env-file change loading?

bash
# Replace the default file resolution with specific files
bun --env-file=.env.ci src/index.ts

# Multiple files — later flags override earlier ones
bun --env-file=.env --env-file=.env.secrets run build

# Disable .env loading entirely; only real env vars exist
bun --no-env-file src/index.ts

You can also switch auto-loading off permanently with env = false in bunfig.toml (explicit --env-file flags still load). Turning the magic off in production and CI is an underrated move: it makes the environment the single source of truth and eliminates the "stale .env left on the server" class of bug.

How is Bun different from Node.js here?

BehaviorBun 1.3Node.js 24
.env loadingAutomatic, zero configOpt-in: --env-file=.env or process.loadEnvFile()
Mode-specific files (.env.production)Automatic via NODE_ENVManual — pass the file you want
$VAR expansionBuilt inNot supported; needs dotenv-expand
Accessprocess.env + aliasesprocess.env only
dotenv package needed?NoNo for basics (20.6+), yes for expansion/override

Both runtimes agree on the rule that matters most: real environment variables beat file values. Code written against process.env ports cleanly in either direction — the only migration step from Node is deleting the dotenv import.

How do you type env variables in TypeScript?

Bun ships an Env interface you can merge into — one declaration types process.env, Bun.env, and import.meta.env at once:

typescript
// env.d.ts
declare module 'bun' {
  interface Env {
    DATABASE_URL: string;
    API_KEY: string;
    LOG_LEVEL?: 'debug' | 'info' | 'warn' | 'error';
  }
}

// anywhere — now a non-optional string with autocomplete
const db = process.env.DATABASE_URL;

Types are a compile-time promise, not a runtime check — a missing DATABASE_URL is still undefined at runtime. Validate required values at startup (zod and envalid both run fine under Bun) so the process fails fast with a named missing key instead of a downstream type error.

Gotcha: bun build inlines env values into the bundle

Secrets can end up inside compiled executables

With bun build --target=bun (and --compile for single-file executables), process.env.FOO expressions can be inlined with whatever value was present at build time — including values silently auto-loaded from your local .env. Developers have found API keys baked into shipped binaries this way (oven-sh/bun#11191). Build in a clean environment (--no-env-file, no exported secrets), and treat anything that was in scope during a build as potentially embedded. Strings-search your artifact before publishing if in doubt.

When should you not rely on Bun's auto-loading?

  • Production and CI — a forgotten .env on a server overrides nothing but still seeds defaults invisibly. Run with --no-env-file and inject real variables.
  • Code that must run on Node too — libraries and dual-runtime tools cannot assume auto-loading or $VAR expansion exists. Document the required variables and let the application layer load files.
  • Build steps for client-side bundles — values inlined at build time are world-readable in the artifact. Keep secrets out of the build environment entirely.

Frequently Asked Questions

Do I need the dotenv package with Bun?

No. Bun reads .env, .env.{NODE_ENV}, and .env.local automatically at startup and even performs $VARIABLE expansion, covering both dotenv and dotenv-expand. The only reasons to keep dotenv are dual-runtime code that also runs on Node, or libraries that bundle it as a dependency anyway.

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

Nothing functional — Bun documents Bun.env and import.meta.env as aliases of process.env; all three point at the same object. Prefer process.env for portability with Node.js and the npm ecosystem.

In what order does Bun load .env files?

Increasing precedence: .env first, then the NODE_ENV-specific file (.env.development by default, .env.production or .env.test when NODE_ENV says so), then .env.local on top. Variables already set in the real environment are never overwritten by any file.

Why do my tests see different env values in Bun?

bun test sets NODE_ENV=test, so Bun loads .env.test instead of .env.development. A value defined only in .env.development disappears under bun test; either duplicate it in .env.test or move shared values into the base .env file.

How do I stop Bun from loading .env files?

Pass --no-env-file on the command line, or set env = false in bunfig.toml. Files named via --env-file still load explicitly. This is the recommended posture in CI and production, where the injected environment should be the single source of truth.

Does bun build embed my environment variables?

It can. With --target=bun or --compile, process.env.X references may be inlined with build-time values, including ones auto-loaded from a local .env (tracked in oven-sh/bun#11191). Build with --no-env-file in a clean environment so secrets cannot leak into shipped artifacts.

References

Validate your .env with the env validator, compare runtimes in the Bun vs Deno vs Node comparison, or read the Node.js env variables guide for the other side of the table.

Was this helpful?

Frequently Asked Questions

Do I need the dotenv package with Bun?

No. Bun reads .env, .env.{NODE_ENV}, and .env.local automatically at startup and even performs $VARIABLE expansion, covering both dotenv and dotenv-expand. The only reasons to keep dotenv are dual-runtime code that also runs on Node, or libraries that bundle it as a dependency anyway.

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

Nothing functional — Bun documents Bun.env and import.meta.env as aliases of process.env; all three point at the same object. Prefer process.env for portability with Node.js and the npm ecosystem.

In what order does Bun load .env files?

Increasing precedence: .env first, then the NODE_ENV-specific file (.env.development by default, .env.production or .env.test when NODE_ENV says so), then .env.local on top. Variables already set in the real environment are never overwritten by any file.

Why do my tests see different env values in Bun?

bun test sets NODE_ENV=test, so Bun loads .env.test instead of .env.development. A value defined only in .env.development disappears under bun test; either duplicate it in .env.test or move shared values into the base .env file.

How do I stop Bun from loading .env files?

Pass --no-env-file on the command line, or set env = false in bunfig.toml. Files named via --env-file still load explicitly. This is the recommended posture in CI and production, where the injected environment should be the single source of truth.

Does bun build embed my environment variables?

It can. With --target=bun or --compile, process.env.X references may be inlined with build-time values, including ones auto-loaded from a local .env (tracked in oven-sh/bun#11191). Build with --no-env-file in a clean environment so secrets cannot leak into shipped artifacts.

Stay up to date

Get notified about new guides, tools, and cheatsheets.