Skip to main content
Bun automatically loads environment variables from .env files, making configuration management seamless across development, testing, and production environments.

Quick Start

Create a .env file in your project root:
.env
DATABASE_URL=postgresql://localhost/mydb
API_KEY=secret_key_123
PORT=3000
Access variables in your code:
Access environment variables
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(process.env.PORT);

// Or use Bun.env
console.log(Bun.env.DATABASE_URL);
No configuration needed - Bun loads .env automatically!

.env File Priority

Bun loads multiple .env files in priority order:
1

Process Environment

Variables already set in the shell (highest priority)
2

.env.local

Local overrides (gitignored by convention)
3

.env.[environment].local

Environment-specific local overrides:
  • .env.development.local
  • .env.production.local
  • .env.test.local
4

.env.[environment]

Environment-specific configuration:
  • .env.development
  • .env.production
  • .env.test
5

.env

Base configuration (lowest priority)
Environment detection:
  • NODE_ENV=production → loads .env.production
  • NODE_ENV=development → loads .env.development (default)
  • NODE_ENV=test → loads .env.test
Implementation: src/env_loader.zig:1-18 - .env file detection

.env File Format

Standard .env syntax:
.env syntax
# Comments start with #
BASIC=value

# Quotes (optional for simple values)
QUOTED="value with spaces"
SINGLE='value with spaces'

# Multiline values
MULTILINE="line 1
line 2
line 3"

# Variable expansion
BASE_URL=https://api.example.com
API_ENDPOINT=${BASE_URL}/v1

# Empty values
EMPTY=
EMPTY_QUOTED=""

# Special characters
PASSWORD="p@ssw0rd!#$%"

# Export syntax (ignored by Bun)
export EXPORTED=value

Escaping

Escaping special characters
# Escape quotes
QUOTE="He said \"hello\""

# Escape newlines
NEWLINE="Line 1\nLine 2"

# Escape dollar signs  
LITERAL_DOLLAR="Price is \$100"

# Backslashes
PATH="C:\\Users\\Name"

Accessing Variables

process.env

Node.js-compatible API:
process.env
// Read variables
const dbUrl = process.env.DATABASE_URL;
const port = process.env.PORT || "3000";

// Check if variable exists
if (process.env.API_KEY) {
  console.log("API key configured");
}

// Set variables (runtime only)
process.env.DYNAMIC_VAR = "value";

// Delete variables
delete process.env.TEMP_VAR;

// Iterate all variables
for (const [key, value] of Object.entries(process.env)) {
  console.log(`${key}=${value}`);
}

Bun.env

Bun-specific API (read-only):
Bun.env
// Read variables (same as process.env)
const dbUrl = Bun.env.DATABASE_URL;
const apiKey = Bun.env.API_KEY;

// Type-safe access
console.log(Bun.env.PORT); // string | undefined

// Cannot modify
// Bun.env.NEW_VAR = "value"; // Error: read-only
Differences:
  • Bun.env is read-only
  • process.env allows runtime modifications
  • Both reference the same underlying data

Custom .env Files

Load specific .env files:

CLI Flag

Custom env file
# Load specific file
bun --env-file=.env.staging run app.ts

# Load multiple files (last wins)
bun --env-file=.env --env-file=.env.local run app.ts

bunfig.toml

bunfig.toml
[env]
# Disable default .env loading
file = false

# Or specify custom files
file = [".env", ".env.custom"]
Implementation: src/bunfig.zig:151-186 - Env config parsing

Programmatic Loading

Manual loading
import { file } from "bun";

// Read and parse .env file
const envFile = await file(".env.production").text();
const lines = envFile.split("\n");

for (const line of lines) {
  const match = line.match(/^([^=]+)=(.*)$/);
  if (match) {
    const [, key, value] = match;
    process.env[key] = value.replace(/^["']|["']$/g, "");
  }
}

Environment-Specific Configuration

Development

.env.development
NODE_ENV=development
DATABASE_URL=postgresql://localhost/dev
LOG_LEVEL=debug
ENABLE_HOT_RELOAD=true

Production

.env.production
NODE_ENV=production
DATABASE_URL=postgresql://prod.example.com/db
LOG_LEVEL=error
ENABLE_HOT_RELOAD=false

Testing

.env.test
NODE_ENV=test
DATABASE_URL=postgresql://localhost/test
LOG_LEVEL=silent
MOCK_API=true

Local Overrides

.env.local (gitignored)
# Override for local development
DATABASE_URL=postgresql://localhost/myusername_dev
API_KEY=my_personal_test_key
DEBUG=true
Add to .gitignore:
.gitignore
.env.local
.env.*.local

Type Safety

Declare Types

env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string;
      API_KEY: string;
      PORT?: string;
      NODE_ENV: "development" | "production" | "test";
    }
  }
}

export {};

Validate Variables

Validation
function getEnv(key: string): string {
  const value = process.env[key];
  if (!value) {
    throw new Error(`Missing environment variable: ${key}`);
  }
  return value;
}

// Required variables
const DATABASE_URL = getEnv("DATABASE_URL");
const API_KEY = getEnv("API_KEY");

// Optional with default
const PORT = process.env.PORT || "3000";

Zod Schema

Zod validation
import { z } from "zod";

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(32),
  PORT: z.string().regex(/^\d+$/).transform(Number).default("3000"),
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
});

const env = envSchema.parse(process.env);

export default env;

Special Variables

NODE_ENV

Determines which .env.[environment] file loads:
NODE_ENV
// Check environment
const isDev = process.env.NODE_ENV === "development";
const isProd = process.env.NODE_ENV === "production";
const isTest = process.env.NODE_ENV === "test";

// Conditional behavior
if (isDev) {
  console.log("Development mode");
}

PATH

System executable search paths:
PATH
// Current PATH
console.log(process.env.PATH);

// Add to PATH (runtime)
process.env.PATH = `/usr/local/bin:${process.env.PATH}`;

HOME / USERPROFILE

User home directory:
Home directory
const home = process.env.HOME || process.env.USERPROFILE;
console.log(`User home: ${home}`);

Proxy Variables

HTTP/HTTPS proxy configuration:
Proxy variables
HTTP_PROXY=http://proxy.example.com:8080
HTTPS_PROXY=http://proxy.example.com:8080
NO_PROXY=localhost,127.0.0.1,.local
Implementation: src/env_loader.zig:167-198 - HTTP proxy detection

TLS Configuration

TLS variables
# Disable certificate validation (development only!)
NODE_TLS_REJECT_UNAUTHORIZED=0
Implementation: src/env_loader.zig:144-161 - TLS reject unauthorized

Security Best Practices

Never commit .env files containing secrets to version control!

1. Gitignore .env Files

.gitignore
# Environment variables
.env
.env.local
.env.*.local

# Keep example file
!.env.example

2. Use .env.example

Commit a template without secrets:
.env.example
DATABASE_URL=postgresql://localhost/mydb
API_KEY=
PORT=3000
NODE_ENV=development

3. Validate Required Variables

Startup validation
const required = ["DATABASE_URL", "API_KEY"];

for (const key of required) {
  if (!process.env[key]) {
    console.error(`Missing required environment variable: ${key}`);
    process.exit(1);
  }
}

4. Restrict Permissions

File permissions
# Make .env readable only by owner
chmod 600 .env

5. Use Secrets Management

For production, use proper secrets management:
  • AWS Secrets Manager
  • HashiCorp Vault
  • Azure Key Vault
  • Google Secret Manager
Fetch from secrets manager
import { SecretsManager } from "@aws-sdk/client-secrets-manager";

const client = new SecretsManager({ region: "us-east-1" });
const secret = await client.getSecretValue({ SecretId: "prod/api-key" });

process.env.API_KEY = JSON.parse(secret.SecretString).apiKey;

Environment Loading Implementation

Bun’s environment variable loading:
Loader structure (src/env_loader.zig:7-29)
pub const Loader = struct {
    map: *Map,
    allocator: std.mem.Allocator,

    @".env.local": ?logger.Source = null,
    @".env.development": ?logger.Source = null,
    @".env.production": ?logger.Source = null,
    @".env.test": ?logger.Source = null,
    @".env.development.local": ?logger.Source = null,
    @".env.production.local": ?logger.Source = null,
    @".env.test.local": ?logger.Source = null,
    @".env": ?logger.Source = null,

    custom_files_loaded: bun.StringArrayHashMap(logger.Source),

    quiet: bool = false,
};
Loading order:
  1. Check each environment file
  2. Parse key=value pairs
  3. Expand variables (${VAR})
  4. Merge into environment (lower priority files don’t override)

Advanced Usage

Environment Detection

Environment detection
class Config {
  static isProduction() {
    return process.env.NODE_ENV === "production";
  }

  static isDevelopment() {
    return !process.env.NODE_ENV || process.env.NODE_ENV === "development";
  }

  static isTest() {
    return process.env.NODE_ENV === "test";
  }

  static isCi() {
    return !!(
      process.env.CI ||
      process.env.GITHUB_ACTIONS ||
      process.env.GITLAB_CI ||
      process.env.CIRCLECI
    );
  }
}
Implementation: src/env_loader.zig:70-76 - CI detection

Dynamic Configuration

Dynamic config
interface AppConfig {
  database: {
    url: string;
    pool: { min: number; max: number };
  };
  api: {
    url: string;
    key: string;
  };
}

function loadConfig(): AppConfig {
  const env = process.env.NODE_ENV || "development";

  return {
    database: {
      url: process.env.DATABASE_URL!,
      pool: {
        min: Number(process.env.DB_POOL_MIN || 2),
        max: Number(process.env.DB_POOL_MAX || 10),
      },
    },
    api: {
      url: process.env.API_URL!,
      key: process.env.API_KEY!,
    },
  };
}

const config = loadConfig();

Variable Expansion

Variable expansion
BASE_URL=https://api.example.com
API_V1=${BASE_URL}/v1
API_V2=${BASE_URL}/v2
FULL_ENDPOINT=${API_V1}/users

# Results in:
# API_V1=https://api.example.com/v1
# API_V2=https://api.example.com/v2  
# FULL_ENDPOINT=https://api.example.com/v1/users

Debugging

Print environment
console.log(process.env);

// Or formatted
for (const [key, value] of Object.entries(process.env)) {
  console.log(`${key}=${value}`);
}

Check Variable Loading

Debug env loading
BUN_DEBUG_QUIET_LOGS=0 bun run app.ts 2>&1 | grep -i env

Verify .env Parsing

Test .env file
const content = await Bun.file(".env").text();
console.log("Raw .env content:");
console.log(content);

console.log("\nParsed variables:");
for (const line of content.split("\n")) {
  if (line && !line.startsWith("#")) {
    console.log(line);
  }
}

Troubleshooting

Variables Not Loading

  1. Check file location: .env must be in project root
  2. Check file name: Must be exactly .env (no spaces)
  3. Check syntax: KEY=value (no spaces around =)
  4. Check NODE_ENV: Correct environment file loaded?

Variables Undefined

Check if loaded
if (!process.env.API_KEY) {
  console.error("API_KEY not found in environment");
  console.log("Available keys:", Object.keys(process.env));
}

Priority Issues

Check which file sets the variable:
Check all .env files
grep "API_KEY" .env*

Build docs developers (and LLMs) love