Hardcoding database credentials, API keys, and feature flags in your source code is one of the most common — and most dangerous — mistakes in web development. Environment variables separate configuration from code, letting you deploy the same codebase to development, staging, and production with different settings.

The 12-Factor App Principle

The 12-Factor App methodology, created by Heroku's founders, states that configuration should be stored in the environment, never in the codebase. This means:

  • Database URLs, API keys, and secrets are never committed to version control
  • The same code runs in every environment; only the configuration changes
  • Configuration is injected via environment variables at runtime

Using .env Files

.env file
# Database
DATABASE_URL=postgres://user:password@localhost:5432/myapp_dev
DATABASE_POOL_SIZE=10

# API Keys
STRIPE_SECRET_KEY=sk_test_abc123
SENDGRID_API_KEY=SG.xxxxx

# App Configuration
NODE_ENV=development
PORT=3000
APP_URL=http://localhost:3000

# Feature Flags
ENABLE_NEW_CHECKOUT=true
ENABLE_DARK_MODE=false

Reading Environment Variables

Node.js (with dotenv)
// Load .env file in development
import 'dotenv/config';

// Access variables
const dbUrl = process.env.DATABASE_URL;
const port = parseInt(process.env.PORT || '3000', 10);
const isDev = process.env.NODE_ENV === 'development';

// Validate required variables at startup
const required = ['DATABASE_URL', 'STRIPE_SECRET_KEY'];
for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing required env var: ${key}`);
  }
}
PHP
// Native PHP
$dbUrl = getenv('DATABASE_URL');
$debug = getenv('APP_DEBUG') === 'true';

// With vlucas/phpdotenv (Composer)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dotenv->required(['DATABASE_URL', 'APP_KEY'])->notEmpty();

// Access via $_ENV
$apiKey = $_ENV['STRIPE_SECRET_KEY'];

Critical Rule: .gitignore

.gitignore
# NEVER commit these
.env
.env.local
.env.production

# DO commit this (template without real values)
# .env.example

Create a .env.example file with all the required variables but placeholder values. This documents what variables your app needs without exposing secrets.

.env.example
DATABASE_URL=postgres://user:password@localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_your_key_here
NODE_ENV=development
PORT=3000

Environment-Specific Configuration

  • Development: Debug mode on, verbose logging, local database, test API keys
  • Staging: Debug mode off, production-like config, staging API keys
  • Production: Debug mode off, minimal logging, production database, real API keys, HTTPS enforced

Secrets Management in Production

For production, don't rely on .env files on the server. Use proper secrets management:

  • Cloud provider secrets: AWS Secrets Manager, Google Secret Manager, Azure Key Vault
  • HashiCorp Vault: Self-hosted secrets management with rotation and audit logging
  • CI/CD secrets: GitHub Actions secrets, GitLab CI variables, Vercel environment variables
  • Docker: Docker secrets, or inject via docker run -e or docker-compose.yml

Common Mistakes

  • Committing .env to Git — even once. If you've done this, rotate all secrets immediately.
  • Using the same API keys for dev and prod — always use test/sandbox keys in development.
  • Not validating env vars at startup — fail fast with a clear error message instead of crashing later with a cryptic error.
  • Accessing process.env everywhere — centralize config access in a single module.

Try Our Free Developer Tools

Encode and decode secrets, format config files instantly.