Programming Beginner 6 min

How to Manage Environment Variables Safely in Node.js

Environment variables are how you keep secrets out of source code. A database password committed to a public repo is exposed the moment it is pushed, and removing it from history is painful. The .env pattern is simple and universally supported — but it has a few rules that are easy to get wrong.

Step-by-step

  1. 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 .env is 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. 2

    Commit a .env.example with safe placeholders

    .env.example is 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. 3

    Load .env natively in Node 20.6+

    Node.js 20.6 added the --env-file flag. No library needed — pass the flag when starting the process and Node populates process.env from 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. 4

    Use dotenv for Node 18 and older

    If you are on an older Node version or need more control over loading order, dotenv is the standard library. Call it at the very top of your entry file — before any other imports that read process.env.

    javascript
    npm 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. 5

    Validate environment variables at startup

    Fail fast. A missing DATABASE_URL should 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. 6

    Use per-environment files carefully

    For different configurations per environment, use separate files. Load the base .env first, then the environment-specific file to override specific values. Only ever commit the .example versions — 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. 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

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.

#Node.js #Config #Env
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.