Learn how to use environment variables properly — in Node.js, Docker, CI/CD, and production — without leaking secrets or breaking deployments.
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.
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
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.
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
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.
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
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.
For production applications, a .env file on a server is a security risk. Use a secrets manager instead:
Example with Doppler:
doppler run -- node server.js
# Doppler injects env vars at runtime — no .env file needed on the server
.env is in .gitignore.env.example is committed with placeholder valuesEnvironment 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.