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.
Part of the guide
BunAt a glance
- Env file that wins
.env.$(NODE_ENV).localFirst 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
- Crunu command
crunu run --env api/development -- bun --watch src/index.tsRecipeCrunu has no dedicated integration here: this is the general setup.
Common pitfalls
- Bun loads .env files by itself for scripts, tests and even bun install, so a forgotten .env.local can change what any command sees.
- Shell variables beat every .env file, and Bun has no option to reverse that order.
- bun build --env inline copies every process.env value referenced in your code into the bundle. A prefix such as PUBLIC_* is safer.
- When Bun runs as node, for example with --bun, it stops loading .env files automatically to match Node.js.
- .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:
| Precedence | File | Notes |
|---|---|---|
| 1 | .env.production.local, .env.development.local or .env.test.local | chosen by NODE_ENV |
| 2 | .env.local | not loaded when NODE_ENV=test |
| 3 | .env.production, .env.development or .env.test | chosen by NODE_ENV |
| 4 | .env | always |
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=FILEreplaces 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-fileturns automatic loading off for one command. Explicit--env-filefiles still load.env = falseinbunfig.tomlturns 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:
| Setting | Effect |
|---|---|
disable (the CLI default) | nothing is inlined |
inline | every 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.
- After
crunu login, start the dev server withcrunu run --env api/development -- bun --watch src/index.ts. - Run tests with
crunu run --env api/test -- bun test --no-env-file, so a local.env.test.localcan't quietly mix in. - 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. - 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_ENVand 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.examplethat 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.
Related reading
- Node.js loads files only when asked.
- Vite describes the conflict between Bun's preloading and Vite modes.
- How to share .env files with your team.
- crunu run for the allowlist, signals and exit codes.
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.
- Bun: Environment variables · checked October 30, 2026
- Bun: Bundler · checked October 30, 2026
- Bun issue 25100: let .env files override system environment variables · checked October 30, 2026