← Back to Blog

.env File Best Practices: Managing Environment Variables Like a Pro

March 9, 2026 8 min read By CodeTidy Team

Environment variables are how modern applications handle configuration — database URLs, API keys, feature flags, and service credentials. The .env file pattern, popularized by the Twelve-Factor App methodology, keeps these values out of your codebase. But .env files come with their own set of problems: secret sprawl, configuration drift between environments, missing variables that crash production, and developers accidentally committing credentials.

This guide covers everything from .env basics to battle-tested practices for teams.

.env File Syntax

# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
DATABASE_POOL_SIZE=10

# API Keys
STRIPE_SECRET_KEY=sk_test_4eC39HqLyjWDarjtT1zdp7dc
STRIPE_WEBHOOK_SECRET=whsec_test_secret

# Application
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug

# Feature Flags
ENABLE_NEW_DASHBOARD=true
ENABLE_BETA_API=false

# Multi-line values (quotes required)
RSA_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0Z3VS5JJcds...
-----END RSA PRIVATE KEY-----"

# Values with spaces (quotes required)
APP_NAME="My Cool App"

# Empty values
SENTRY_DSN=

Rules: one variable per line, KEY=VALUE format, no spaces around =, # for comments. Quotes are optional for simple values but required for multi-line values or values containing spaces.

The .env.example Pattern

Every project should have a .env.example file checked into version control:

# .env.example — committed to git
# Copy to .env and fill in real values

# Database (required)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname

# Stripe (required for payments)
STRIPE_SECRET_KEY=sk_test_YOUR_KEY_HERE
STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET_HERE

# Application
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug

# Optional: Sentry error tracking
SENTRY_DSN=

This file serves three purposes:

  1. Documentation — new developers know exactly which variables are needed
  2. Templatecp .env.example .env gives you a starting point
  3. Drift detection — compare .env against .env.example to find missing or extra variables

Never Commit .env Files

Your .gitignore should always include:

# .gitignore
.env
.env.local
.env.*.local
.env.production
.env.staging

# DO commit these:
# .env.example
# .env.test (if it contains no real secrets)

If you've already committed a .env file, removing it from git isn't enough — it's in the history forever. You need to:

  1. Remove the file from tracking: git rm --cached .env
  2. Add to .gitignore
  3. Commit and push
  4. Rotate every credential that was in the file — assume they're compromised
  5. Optionally, rewrite git history with git filter-branch or BFG Repo-Cleaner

Environment-Specific Files

Most frameworks support a hierarchy of .env files:

.env                # Default values (committed or not — team preference)
.env.local          # Local overrides (never committed)
.env.development    # Development-specific
.env.staging        # Staging-specific
.env.production     # Production-specific
.env.test           # Test environment

Loading order typically follows a priority chain: .env.local overrides .env.{'{'}environment{'}'} which overrides .env. Check your framework's documentation — Next.js, Vite, Rails, and Laravel each handle this slightly differently.

Detecting Missing Variables

The most common .env bug: a variable exists in production but is missing from a developer's local .env, or vice versa. The app starts fine but crashes when it tries to use the missing variable.

Validate at Startup

// validate-env.js — run at application startup
const required = [
  'DATABASE_URL',
  'STRIPE_SECRET_KEY',
  'JWT_SECRET',
];

const missing = required.filter(key => !process.env[key]);

if (missing.length > 0) {
  console.error('Missing required environment variables:');
  missing.forEach(key => console.error('  - ' + key));
  process.exit(1);
}

Use a Schema Library

// With zod (recommended)
import { z } from 'zod';

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z.enum(['development', 'staging', 'production']),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});

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

Schema validation catches not just missing variables but wrong types, invalid formats, and missing defaults. Libraries like zod, envalid, and @t3-oss/env-core all work well for this.

Diff Your .env Files

Use our .env Diff & Merge tool to compare your .env against .env.example — it highlights missing keys, extra keys, and changed values instantly.

Secrets Management for Teams

For teams, sharing .env files via Slack, email, or shared drives is a security antipattern. Better approaches:

ApproachBest ForCost
1Password / Bitwarden shared vaultsSmall teams$4-8/user/mo
Doppler / InfisicalDedicated secrets managementFree tier available
AWS Secrets ManagerAWS-heavy teams$0.40/secret/mo
HashiCorp VaultEnterprise, multi-cloudFree (self-hosted)
GitHub Actions SecretsCI/CD onlyFree
git-cryptEncrypted files in repoFree

The goal: no developer should need to manually copy-paste secrets.

CI/CD Environment Variables

In CI/CD pipelines, environment variables are set differently than local development:

# GitHub Actions — use secrets context
# DATABASE_URL: secrets.DATABASE_URL

# Docker
docker run -e DATABASE_URL=postgres://... myapp

# Docker Compose
services:
  app:
    env_file:
      - .env.production

# Kubernetes (from Secret)
envFrom:
  - secretRef:
      name: myapp-secrets

Never bake secrets into Docker images. Use runtime environment variables or mounted secrets.

Common .env Mistakes

  1. Committing .env to git — always add to .gitignore before your first commit
  2. Using production credentials locally — use separate credentials per environment
  3. Not rotating secrets — if a secret might be exposed, rotate it immediately
  4. Hardcoding fallback valuesprocess.env.DB_URL || "postgres://..." hides missing config
  5. Inconsistent naming — pick a convention (SCREAMING_SNAKE_CASE) and stick to it
  6. No .env.example — new team members shouldn't have to guess which variables are needed
  7. Storing non-secrets in .env — public config can go in a regular config file
  8. Sharing .env via Slack/email — use a secrets manager instead

Quick Reference: .env Loading Libraries

LanguageLibraryUsage
Node.jsdotenvrequire('dotenv').config()
Node.js (built-in)Node 20.6+node --env-file=.env app.js
Pythonpython-dotenvload_dotenv()
Rubydotenv gemDotenv.load
Gogodotenvgodotenv.Load()
PHPvlucas/phpdotenvDotenv::createImmutable(__DIR__)->load()
Rustdotenvydotenvy::dotenv().ok()

Compare your .env files side by side with our .env Diff & Merge tool — find missing keys, detect changes, and generate merged configs instantly.

Drop file to load