Environment variables are the standard way to configure applications across different environments. They separate configuration from code, keep secrets out of repositories, and enable the same app to run differently in development, staging, and production. This guide covers everything from basics to advanced patterns.
Key Takeaways
- 1.env.local for secrets (gitignored), .env.example for templates (committed)
- 2NEXT_PUBLIC_, REACT_APP_, VITE_ prefixes expose vars to browser—never use for secrets
- 3Validate environment variables at startup with clear error messages
- 4Use secret managers (AWS Secrets Manager, Vault) in production, not plain env vars
- 5Restart your server/process after changing .env files—they’re read at startup
1What Are Environment Variables?
Environment variables are key-value pairs set at the operating system or process level. Your application reads them at runtime instead of hardcoding values.
# Set environment variable (Linux/macOS)
export DATABASE_URL="postgres://user:pass@localhost/mydb"
# Set environment variable (Windows CMD)
set DATABASE_URL=postgres://user:pass@localhost/mydb
# Set environment variable (Windows PowerShell)
$env:DATABASE_URL = "postgres://user:pass@localhost/mydb"
# View all environment variables
printenv # Linux/macOS
set # Windows CMD
Get-ChildItem Env: # Windows PowerShell
# View specific variable
echo $DATABASE_URL # Linux/macOS
echo %DATABASE_URL% # Windows CMD
echo $env:DATABASE_URL # Windows PowerShell| Use Case | Example Variables |
|---|---|
| Database connections | DATABASE_URL, REDIS_URL |
| API keys and secrets | API_KEY, JWT_SECRET |
| Feature flags | ENABLE_BETA, DEBUG_MODE |
| Service URLs | API_BASE_URL, CDN_URL |
| Environment identifier | NODE_ENV, APP_ENV |
| Port and host | PORT, HOST |
Using .env Files
.env files store environment variables in a file that's loaded at application startup. They're the standard for local development.
# .env file example
# Database
DATABASE_URL=postgres://user:password@localhost:5432/myapp
REDIS_URL=redis://localhost:6379
# Authentication
JWT_SECRET=your-super-secret-key-here
SESSION_SECRET=another-random-secret
# Third-party APIs
STRIPE_SECRET_KEY=sk_test_...
SENDGRID_API_KEY=SG....
# Feature flags
ENABLE_NEW_DASHBOARD=true
DEBUG=false
# Environment
NODE_ENV=development
PORT=3000
# Note: No spaces around =
# Note: Quotes optional unless value has spaces/special chars
APP_NAME="My Awesome App"// Load .env file in Node.js
// Option 1: dotenv package (most common)
import 'dotenv/config'; // ES modules
// or
require('dotenv').config(); // CommonJS
// Option 2: Built-in (Node.js 20.6+)
// Run with: node --env-file=.env app.js
// Access variables
const dbUrl = process.env.DATABASE_URL;
const port = parseInt(process.env.PORT || '3000', 10);
const debug = process.env.DEBUG === 'true';
// With default values
const apiUrl = process.env.API_URL || 'http://localhost:3001';
// Required variable (throw if missing)
function requireEnv(key) {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required env variable: ${key}`);
}
return value;
}
const jwtSecret = requireEnv('JWT_SECRET');**Common .env File Patterns:**
- .env - Default, shared settings (committed to repo)
- .env.local - Local overrides (gitignored)
- .env.development - Development environment
- .env.production - Production environment
- .env.test - Test environment
- .env.example - Template with dummy values (committed)
NEVER commit .env files with real secrets to version control. Add .env and .env.local to .gitignore. Commit only .env.example with placeholder values.
3Framework-Specific Usage
Different frameworks handle environment variables differently. Here are the common patterns.
// Next.js
// Server-side: All env vars available
const secret = process.env.API_SECRET;
// Client-side: Only NEXT_PUBLIC_ prefixed vars
// These are replaced at build time
const publicApiUrl = process.env.NEXT_PUBLIC_API_URL;
// .env.local example for Next.js
// NEXT_PUBLIC_API_URL=https://api.example.com // Available in browser
// API_SECRET=secret123 // Server only// Create React App (Vite uses similar pattern)
// Only REACT_APP_ (CRA) or VITE_ (Vite) prefixed vars
// .env
// REACT_APP_API_URL=https://api.example.com
// VITE_API_URL=https://api.example.com
// Access in code (CRA)
const apiUrl = process.env.REACT_APP_API_URL;
// Access in code (Vite)
const apiUrl = import.meta.env.VITE_API_URL;# Python
import os
from dotenv import load_dotenv
# Load .env file
load_dotenv()
# Access variables
database_url = os.getenv('DATABASE_URL')
port = int(os.getenv('PORT', '8000')) # With default
debug = os.getenv('DEBUG', 'false').lower() == 'true'
# Required variable
def require_env(key: str) -> str:
value = os.getenv(key)
if value is None:
raise ValueError(f"Missing required env variable: {key}")
return value
jwt_secret = require_env('JWT_SECRET')# Docker
# Option 1: Pass at runtime
docker run -e DATABASE_URL=postgres://... myapp
docker run --env-file .env.production myapp
# Option 2: In Dockerfile (not for secrets!)
ENV NODE_ENV=production
ENV PORT=3000
# Option 3: docker-compose.yml
services:
app:
image: myapp
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://db:5432/myapp
env_file:
- .env.production4Cloud Platform Configuration
In production, set environment variables through your hosting platform's dashboard or CLI—not through files.
# Vercel
vercel env add DATABASE_URL production
vercel env pull .env.local # Download to local
# Netlify
netlify env:set DATABASE_URL "postgres://..."
netlify env:import .env # Import from file
# Heroku
heroku config:set DATABASE_URL=postgres://...
heroku config # List all vars
# AWS (Systems Manager Parameter Store)
aws ssm put-parameter \
--name "/myapp/prod/DATABASE_URL" \
--value "postgres://..." \
--type SecureString
# Google Cloud (Secret Manager)
echo -n "postgres://..." | gcloud secrets create DATABASE_URL --data-file=-
# Azure (App Service)
az webapp config appsettings set \
--name myapp \
--resource-group mygroup \
--settings DATABASE_URL="postgres://..."Use your platform\
5Security Best Practices
Environment variables often contain secrets. Handle them carefully.
**Security Checklist:**
- Never commit secrets to version control
- Add .env, .env.local, .env.*.local to .gitignore
- Use different secrets for each environment
- Rotate secrets regularly (API keys, JWT secrets)
- Use secret managers in production (not plain env vars)
- Limit access to production secrets to essential personnel
- Audit secret access and changes
- Never log environment variables (especially at startup)
# .gitignore - Essential entries
.env
.env.local
.env.*.local
.env.development.local
.env.test.local
.env.production.local
# Keep these committed
!.env.example
!.env.development # Only if no secrets# .env.example - Template for developers
# Copy to .env.local and fill in values
# Database (get from team lead)
DATABASE_URL=postgres://user:password@localhost:5432/myapp_dev
# Authentication (generate with: openssl rand -base64 32)
JWT_SECRET=replace-with-random-secret
# Third-party APIs (get from respective dashboards)
STRIPE_SECRET_KEY=sk_test_...
SENDGRID_API_KEY=
# Feature flags
ENABLE_BETA_FEATURES=falseEnvironment variables in browser JavaScript are exposed in the bundle. Only use NEXT_PUBLIC_, REACT_APP_, or VITE_ prefixes for truly public values (never secrets).
6Validation and Types
Validate environment variables at startup to catch configuration errors early.
// TypeScript validation with Zod
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number).default('3000'),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
ENABLE_BETA: z.string().transform(s => s === 'true').default('false'),
API_URL: z.string().url().optional(),
});
// Parse and validate
const env = envSchema.parse(process.env);
// Now you have typed access
console.log(env.PORT); // number
console.log(env.ENABLE_BETA); // boolean
// Export for use throughout app
export { env };
// Usage in other files
import { env } from './config';
const db = connect(env.DATABASE_URL);// Simple validation without external libraries
const requiredVars = [
'DATABASE_URL',
'JWT_SECRET',
'REDIS_URL'
];
const missing = requiredVars.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('Missing required environment variables:');
missing.forEach(key => console.error(` - ${key}`));
process.exit(1);
}
// Type coercion helpers
const config = {
port: parseInt(process.env.PORT || '3000', 10),
debug: process.env.DEBUG === 'true',
maxRetries: parseInt(process.env.MAX_RETRIES || '3', 10),
allowedOrigins: (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean),
};Validate environment variables as early as possible—ideally before your app starts handling requests. Fail fast with clear error messages.
7Common Patterns
Here are patterns for handling environment variables effectively.
// Environment file cascade (Next.js style)
// Load order (later overrides earlier):
// 1. .env (base defaults)
// 2. .env.local (local overrides, gitignored)
// 3. .env.[NODE_ENV] (environment-specific)
// 4. .env.[NODE_ENV].local (local env overrides)
// 5. Actual environment variables (highest priority)
// Manual cascade with dotenv
import dotenv from 'dotenv';
import path from 'path';
const envFiles = [
'.env',
'.env.local',
`.env.${process.env.NODE_ENV || 'development'}`,
`.env.${process.env.NODE_ENV || 'development'}.local`,
];
envFiles.forEach(file => {
dotenv.config({ path: path.resolve(process.cwd(), file) });
});# Namespace your variables for clarity
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=postgres
DB_PASSWORD=secret
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# AWS
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
# Feature flags
FEATURE_NEW_CHECKOUT=true
FEATURE_DARK_MODE=false# URL patterns for services
# Option 1: Full connection string
DATABASE_URL=postgres://user:pass@host:5432/dbname?sslmode=require
# Option 2: Separate components (more flexible)
DB_HOST=db.example.com
DB_PORT=5432
DB_NAME=myapp
DB_USER=appuser
DB_PASSWORD=secret
DB_SSL=true
# Build URL from components in code
const dbUrl = `postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}`;8Troubleshooting
Common issues when working with environment variables and how to fix them.
| Problem | Solution |
|---|---|
| Variable is undefined | Check file name (.env not .env.txt), check spelling, ensure dotenv loaded before access |
| Works locally, not in production | Set variable in hosting platform dashboard, check for typos in key names |
| Variable not updating | Restart server/process—env vars read at startup, not live |
| Spaces breaking values | Wrap value in quotes: KEY="value with spaces" |
| Special characters in value | Escape with backslash or use quotes: KEY="pa\$\$word" |
| Multi-line value | Use quotes and \n: KEY="line1\nline2" or heredoc syntax |
| Browser can't see variable | Use correct prefix (NEXT_PUBLIC_, REACT_APP_, VITE_) |
// Debug: Check if variable is loaded
console.log('DATABASE_URL exists:', !!process.env.DATABASE_URL);
console.log('DATABASE_URL length:', process.env.DATABASE_URL?.length);
// Debug: List all env vars (development only!)
if (process.env.NODE_ENV === 'development') {
console.log('Environment variables:', Object.keys(process.env).sort());
}
// Debug: Check .env file loading
import dotenv from 'dotenv';
const result = dotenv.config({ debug: true });
if (result.error) {
console.error('Error loading .env:', result.error);
}Boost Your Developer Workflow
Free online tools for encoding, formatting, hashing, and more.
Explore Dev ToolsFrequently Asked Questions
What is the difference between .env and .env.local?
.env contains default values that can be committed to version control (no secrets). .env.local contains local overrides and secrets, is gitignored, and never committed. .env.local takes precedence over .env.
Should I commit .env files to Git?
Commit .env and .env.example with non-secret defaults and placeholder values. Never commit .env.local, .env.*.local, or any file containing real secrets. Add them to .gitignore.
How do I use environment variables in the browser?
You can’t directly access process.env in browsers. Frameworks like Next.js, Create React App, and Vite replace specific prefixed variables (NEXT_PUBLIC_, REACT_APP_, VITE_) at build time. Only use these for public values—never secrets.
How should I handle secrets in production?
Use a secret manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) rather than plain environment variables. These provide encryption at rest, access control, audit logging, and secret rotation. Inject secrets at runtime.
Why is my environment variable undefined?
Common causes: 1) .env file not loaded (call dotenv.config() first), 2) typo in variable name, 3) file named incorrectly (.env.txt instead of .env), 4) accessing before loading, 5) process not restarted after changing .env file.