What Are Environment Variables?
Environment variables are key-value pairs stored in the operating system's environment, outside your application code. They let you configure an application differently across environments (development, staging, production) without changing the code.
DATABASE_URL=postgresql://localhost:5432/mydb
SECRET_KEY=abc123
NODE_ENV=production
PORT=3000
The key rule: secrets and config should never be hardcoded in source code. Environment variables are how you keep them out.
The .env File
For local development, the .env file is the standard way to set environment variables. The dotenv library loads it automatically.
# .env
DATABASE_URL=postgresql://localhost:5432/devdb
SECRET_KEY=dev-secret-not-real
STRIPE_SECRET_KEY=sk_test_your_stripe_key_here
PORT=3000
NODE_ENV=development
Node.js with dotenv:
npm install dotenv
// At the very top of your entry file
import "dotenv/config";
// or
require("dotenv").config();
const dbUrl = process.env.DATABASE_URL;
Node.js 20.6+ has built-in .env support — no package needed:
node --env-file=.env server.js
CRITICAL: Add .env to .gitignore
Never commit .env to version control. This is the most common way secrets get leaked.
# .gitignore
.env
.env.local
.env.production
Instead, commit a .env.example file with placeholder values:
# .env.example — safe to commit
DATABASE_URL=postgresql://localhost:5432/yourdb
SECRET_KEY=your-secret-key-here
STRIPE_SECRET_KEY=sk_test_your-key
PORT=3000
NODE_ENV=development
This documents what variables are needed without exposing real values.
Naming Conventions
Use SCREAMING_SNAKE_CASE. Be descriptive:
# Good
DATABASE_URL=...
STRIPE_SECRET_KEY=...
SENDGRID_API_KEY=...
NEXT_PUBLIC_SITE_URL=... # NEXT_PUBLIC_ prefix = exposed to browser in Next.js
# Bad
DB=...
KEY=...
SECRET=...
Group related variables with a prefix:
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret
POSTGRES_DB=myapp
REDIS_HOST=localhost
REDIS_PORT=6379
Validation at Startup
Never let your app start with missing required variables. Catch it immediately:
// env.js — validate on startup
const required = [
"DATABASE_URL",
"SECRET_KEY",
"STRIPE_SECRET_KEY",
];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`);
}
}
export const env = {
databaseUrl: process.env.DATABASE_URL,
secretKey: process.env.SECRET_KEY,
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
port: parseInt(process.env.PORT || "3000", 10),
nodeEnv: process.env.NODE_ENV || "development",
};
For more robust validation, use zod:
npm install zod
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
SECRET_KEY: z.string().min(32),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "staging", "production"]).default("development"),
});
export const env = envSchema.parse(process.env);
If any variable is missing or invalid, your app crashes on startup with a clear error — far better than a cryptic runtime failure hours later.
Environment Variables in CI/CD
Never put secrets in your docker-compose.yml, Dockerfile, or CI config files.
GitHub Actions:
# .github/workflows/deploy.yml
steps:
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: npm run deploy
Add secrets under Repository Settings → Secrets and Variables → Actions.
Docker:
# Pass at runtime, not in Dockerfile
docker run -e DATABASE_URL=postgresql://... myapp
# Or use --env-file
docker run --env-file .env.production myapp
Never do this:
# WRONG — baked into the image layer, visible in docker history
ENV DATABASE_URL=postgresql://real-host/prod
Multiple Environments
Use separate .env files per environment:
.env.development # local dev
.env.test # test runner
.env.staging # staging server
.env.production # production (NEVER commit this)
Load the right one based on NODE_ENV:
dotenv -e .env.${NODE_ENV} node server.js
Or in Next.js — it handles this automatically: .env.local overrides .env.
Secrets Management at Scale
For production applications, a .env file on a server is a security risk. Use a secrets manager instead:
- AWS Secrets Manager — retrieve secrets programmatically
- HashiCorp Vault — enterprise secrets management
- Doppler — developer-friendly secrets manager, syncs to any platform
- Vercel / Netlify / Railway — built-in environment variable management
Example with Doppler:
doppler run -- node server.js
# Doppler injects env vars at runtime — no .env file needed on the server
Security Checklist
-
.envis in.gitignore -
.env.exampleis committed with placeholder values - All required variables are validated at startup
- Secrets are not logged (especially in error handlers)
- Different secrets per environment (dev secrets ≠ prod secrets)
- Rotate secrets if they're ever exposed
- Use a secrets manager in production
Conclusion
Environment variables are the simplest and most universal way to configure applications. The rules are straightforward: never hardcode secrets, never commit .env, always validate on startup, and use a proper secrets manager for production. Following these practices prevents the most common class of security incidents — accidentally exposed credentials.