Step-by-step
-
1
Add .env to .gitignore immediately
Do this before creating the file. Once a file is committed and pushed, it exists in git history even after deletion. If
.envis not already in your.gitignore, add it now.bash# .gitignore .env .env.local .env.production .env.staging # Never commit the real env files — only .env.example -
2
Commit a .env.example with safe placeholders
.env.exampleis the contract between developers. It documents every variable the app needs, with placeholder values instead of real secrets. New team members copy it and fill in their own values.bash# .env.example — committed to git, no real values NODE_ENV=development PORT=3000 DATABASE_URL=postgres://user:password@localhost:5432/myapp REDIS_URL=redis://localhost:6379 JWT_SECRET=replace-with-a-long-random-string STRIPE_SECRET_KEY=sk_test_replace_me # Get this from the AWS IAM console AWS_ACCESS_KEY_ID=REPLACE_ME AWS_SECRET_ACCESS_KEY=REPLACE_ME -
3
Load .env natively in Node 20.6+
Node.js 20.6 added the
--env-fileflag. No library needed — pass the flag when starting the process and Node populatesprocess.envfrom the file.bash# Start your app node --env-file=.env server.js # Or in package.json scripts: # "start": "node --env-file=.env server.js" # "dev": "node --env-file=.env --watch server.js" // In your code — process.env is already populated const port = process.env.PORT ?? 3000; const db = process.env.DATABASE_URL; -
4
Use dotenv for Node 18 and older
If you are on an older Node version or need more control over loading order,
dotenvis the standard library. Call it at the very top of your entry file — before any other imports that readprocess.env.javascriptnpm install dotenv // server.js — must be the very first thing require('dotenv').config(); // Or with ES modules: // import 'dotenv/config'; const port = process.env.PORT ?? 3000; -
5
Validate environment variables at startup
Fail fast. A missing
DATABASE_URLshould crash the process immediately at startup with a clear error — not silently fail on the first database call ten minutes into a production deployment. Validate with zod for a typed result, or a minimal manual check.javascript// Option A: manual check (zero dependencies) const required = ['DATABASE_URL', 'JWT_SECRET', 'PORT']; for (const key of required) { if (!process.env[key]) { console.error(`Missing required environment variable: ${key}`); process.exit(1); } } // Option B: zod (typed + defaults) // npm install zod const { z } = require('zod'); const envSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), PORT: z.string().transform(Number).default('3000'), DATABASE_URL: z.string().url(), JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'), }); const env = envSchema.parse(process.env); // throws on invalid module.exports = env; -
6
Use per-environment files carefully
For different configurations per environment, use separate files. Load the base
.envfirst, then the environment-specific file to override specific values. Only ever commit the.exampleversions — never the real ones.javascript# .env — base values (gitignored) # .env.production — production overrides (gitignored) # .env.staging — staging overrides (gitignored) # .env.example — safe placeholder template (committed) # .env.production.example — committed // Loading order with dotenv (base first, then environment override): require('dotenv').config({ path: '.env' }); require('dotenv').config({ path: `.env.${process.env.NODE_ENV}`, override: true, }); -
7
Rotate secrets when someone leaves the team
When a team member leaves, assume every secret they had access to is compromised. Rotate them before the offboarding is complete, not after. The rotation process should be documented and practiced — finding out how to rotate a Stripe key at 2 am during an incident is not the right time.
Practical rotation checklist:
- Generate new values for every secret in
.env.example - Update the secrets in your secrets manager (AWS Secrets Manager, 1Password Teams, Doppler) — never share plaintext over Slack or email
- Deploy the updated environment to each environment in order: staging → production
- Verify the application is healthy, then revoke the old secrets in the issuing service
- Generate new values for every secret in
Tips & gotchas
- Never log <code>process.env</code> entirely — it will expose all your secrets to whoever has access to your logs. Log only the keys that are present, not their values.
- In production, prefer a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) over <code>.env</code> files on disk. Secrets managers provide audit logs, rotation, and access control.
- The <code>--env-file</code> flag in Node 20.6+ does not override variables that are already set in the shell environment. This is the correct behavior — it lets CI/CD systems inject real secrets without being overridden by a stale <code>.env</code> file.
- Keep secrets short-lived where possible. API tokens with expiry are safer than permanent credentials.
Wrapping up
Three rules cover 90% of env variable hygiene: .env goes in .gitignore before anything else, .env.example is the only env file ever committed, and you validate required variables at startup so a misconfigured deployment fails loudly and immediately. Build these habits from the start of every project.