# 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.

URL: https://crunu.com/environment-variables/bun
Language: en
Updated: 2026-10-30

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](https://bun.com/docs/runtime/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                          |

*Figure: Diagram of how Bun loads environment variables. Env files are read in this order of priority, and the first one wins: .env.$(NODE_ENV).local, .env.local, .env.$(NODE_ENV), and .env. Values are read from the environment when the app starts. There is no public prefix: nothing reaches the browser unless your code sends it.*

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](https://github.com/oven-sh/bun/issues/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:

| 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.

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](https://crunu.com/environment-variables/fly-io). See the [Docker Compose page](https://crunu.com/environment-variables/docker-compose) and [secrets in Docker images](https://crunu.com/blog/secrets-in-docker-images).

## Related reading

- [Node.js](https://crunu.com/environment-variables/nodejs) loads files only when asked.
- [Vite](https://crunu.com/environment-variables/vite) describes the conflict between Bun's preloading and Vite modes.
- [How to share .env files with your team](https://crunu.com/guides/share-env-files-with-a-team).
- [crunu run](https://crunu.com/docs/cli/run) for the allowlist, signals and exit codes.

## FAQ

### 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

- [Bun: Environment variables](https://bun.com/docs/runtime/environment-variables) (accessed 2026-10-30)
- [Bun: Bundler](https://bun.com/docs/bundler) (accessed 2026-10-30)
- [Bun issue 25100: let .env files override system environment variables](https://github.com/oven-sh/bun/issues/25100) (accessed 2026-10-30)
