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:
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:
Process Environment
Variables already set in the shell (highest priority)
.env.local
Local overrides (gitignored by convention)
.env.[environment].local
Environment-specific local overrides:
.env.development.local
.env.production.local
.env.test.local
.env.[environment]
Environment-specific configuration:
.env.development
.env.production
.env.test
.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
Standard .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:
// 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):
// 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
# 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
[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
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
NODE_ENV=development
DATABASE_URL=postgresql://localhost/dev
LOG_LEVEL=debug
ENABLE_HOT_RELOAD=true
Production
NODE_ENV=production
DATABASE_URL=postgresql://prod.example.com/db
LOG_LEVEL=error
ENABLE_HOT_RELOAD=false
Testing
NODE_ENV=test
DATABASE_URL=postgresql://localhost/test
LOG_LEVEL=silent
MOCK_API=true
Local Overrides
# Override for local development
DATABASE_URL=postgresql://localhost/myusername_dev
API_KEY=my_personal_test_key
DEBUG=true
Add to .gitignore:
Type Safety
Declare Types
declare global {
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
API_KEY: string;
PORT?: string;
NODE_ENV: "development" | "production" | "test";
}
}
}
export {};
Validate Variables
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
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:
// 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:
// 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:
const home = process.env.HOME || process.env.USERPROFILE;
console.log(`User home: ${home}`);
Proxy Variables
HTTP/HTTPS proxy configuration:
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
# 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
# Environment variables
.env
.env.local
.env.*.local
# Keep example file
!.env.example
2. Use .env.example
Commit a template without secrets:
DATABASE_URL=postgresql://localhost/mydb
API_KEY=
PORT=3000
NODE_ENV=development
3. Validate Required Variables
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
# 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:
- Check each environment file
- Parse key=value pairs
- Expand variables (
${VAR})
- Merge into environment (lower priority files don’t override)
Advanced Usage
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
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
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 All Variables
console.log(process.env);
// Or formatted
for (const [key, value] of Object.entries(process.env)) {
console.log(`${key}=${value}`);
}
Check Variable Loading
BUN_DEBUG_QUIET_LOGS=0 bun run app.ts 2>&1 | grep -i env
Verify .env Parsing
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
- Check file location:
.env must be in project root
- Check file name: Must be exactly
.env (no spaces)
- Check syntax:
KEY=value (no spaces around =)
- Check NODE_ENV: Correct environment file loaded?
Variables Undefined
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: