Every week, developers accidentally commit API keys to GitHub. Every week, bots scan public repos within seconds and drain crypto wallets or rack up API bills. This guide is how to not be that developer.
What a .env file is
A .env file is a plain text file that stores environment variables — configuration values that change between environments (local, staging, production) and that you never want in your code.
# .env
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
STRIPE_SECRET_KEY=sk_test_abc123
JWT_SECRET=some_long_random_string_here
NODE_ENV=development
PORT=3000
The app reads these at startup. The file never gets committed to git.
The single most important rule
# .gitignore — always add this
.env
.env.local
.env.*.local
Do this before your first commit. If you add .env to .gitignore after accidentally committing it, the file is still in git history and still leakable.
The right file structure
Use these conventions:
.env ← real values, NEVER commit (in .gitignore)
.env.example ← template with dummy values, COMMIT this
.env.local ← local overrides, NEVER commit
.env.production ← production values, NEVER commit (use a secrets manager instead)
.env.example is for your teammates — they clone the repo, copy it, fill in their values:
# .env.example — commit this file
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
STRIPE_SECRET_KEY=sk_test_...
JWT_SECRET=change-me
PORT=3000
REDIS_URL=redis://localhost:6379
# First-time setup for new developers:
cp .env.example .env
# Then edit .env with real values
Loading .env in Node.js
Option 1: Node.js 20.6+ built-in
node --env-file=.env server.js
# Or in package.json scripts:
"scripts": {
"dev": "node --env-file=.env src/index.js",
"start": "node --env-file=.env src/index.js"
}
No packages needed.
Option 2: dotenv package (most common)
npm install dotenv
// At the very top of your entry file (index.js, server.js, app.js)
import 'dotenv/config';
// Or:
import dotenv from 'dotenv';
dotenv.config();
// Now process.env has your variables
const db = new Client({ connectionString: process.env.DATABASE_URL });
Validate required variables at startup
Don’t let your app silently break when a variable is missing. Check early:
const required = ['DATABASE_URL', 'JWT_SECRET', 'STRIPE_SECRET_KEY'];
for (const key of required) {
if (!process.env[key]) {
console.error(`Missing required environment variable: ${key}`);
process.exit(1);
}
}
Or use a library like zod for typed validation:
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
export const env = envSchema.parse(process.env);
// env.PORT is a number, env.DATABASE_URL is a valid URL, etc.
Loading .env in Python
python-dotenv
pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv() # reads .env from current directory
db_url = os.getenv('DATABASE_URL')
secret = os.getenv('JWT_SECRET')
# With a default value
port = int(os.getenv('PORT', '3000'))
Pydantic Settings (FastAPI projects)
pip install pydantic-settings
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
jwt_secret: str
port: int = 3000
debug: bool = False
class Config:
env_file = ".env"
settings = Settings()
# settings.database_url, settings.port, etc.
# Raises ValidationError if required vars are missing or wrong type
Using .env with Docker
docker run
# Load all variables from .env file
docker run --env-file .env myapp
# Or pass individual variables
docker run -e NODE_ENV=production -e PORT=3000 myapp
docker-compose
services:
app:
build: .
env_file:
- .env # loads all variables from .env
environment:
- NODE_ENV=production # can override individual vars
Important: Docker Compose automatically reads .env from the project directory for variable substitution in docker-compose.yml itself:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} # reads from .env
The mistakes that get people in trouble
Committing .env directly
# Do this BEFORE your first commit:
echo ".env" >> .gitignore
git add .gitignore
git commit -m "add .gitignore"
If you’ve already committed .env:
# Remove it from git tracking (keeps the file locally)
git rm --cached .env
git commit -m "remove .env from tracking"
Then rotate all the secrets — assume they’re compromised. GitHub scans for common API key patterns and notifies providers. You may already have an email.
Hardcoding values in code
// Never do this:
const stripe = new Stripe("sk_live_abc123...");
// Do this:
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
Using the same secrets in every environment
Prod keys should only be in production. Use different API keys for dev/staging/prod — most services let you create multiple keys.
Logging environment variables
// Never do this:
console.log('Config:', process.env);
// This leaks EVERYTHING to your logs
Sharing secrets in Slack or email
Use a proper secrets manager (Doppler, 1Password Secrets, AWS Secrets Manager, Vault) or at minimum use a self-destructing link (like onetimesecret.com).
Production: don’t use .env files
For production deployments, use your platform’s native secrets management:
| Platform | Where to set secrets |
|---|---|
| Netlify | Site settings → Environment variables |
| Vercel | Project settings → Environment variables |
| Railway | Project → Variables |
| Render | Service → Environment |
| AWS | Secrets Manager or Parameter Store |
| Heroku | heroku config:set KEY=value |
| Docker | Docker secrets or compose env_file |
| Kubernetes | Kubernetes Secrets |
These inject values as environment variables at runtime — no .env file in the container, no file that can be accidentally copied or logged.
Checklist
-
.envis in.gitignorebefore first commit -
.env.exampleexists with dummy values and IS committed - Required variables are validated at startup
- No secrets hardcoded in source files
- Different API keys for dev vs prod
- Production uses platform secrets management, not a
.envfile - Team knows to run
cp .env.example .envon first setup
Containerizing your app? Learn How to Write a Production Dockerfile for Node.js and keep secrets out of Docker images too.
Related Reading.
How to Write a Production Dockerfile for Node.js
Build small, fast, production-ready Docker images for Node.js — layer caching, multi-stage builds, non-root user, health checks, and .dockerignore explained.
How to Install Docker on Ubuntu, macOS and Windows
Install Docker Desktop or Docker Engine step-by-step on Ubuntu, macOS, and Windows — including post-install setup, running your first container, and Docker Compose.
How to Install Node.js on Ubuntu, macOS and Windows
Install Node.js using nvm, Homebrew, or the official installer — step-by-step for Ubuntu, macOS, and Windows. Includes switching versions and verifying your install.