M
MeshWorld.
HowTo Security Node.js Python Docker Environment Variables Developer Tools Secrets 5 min read

How to Set Up a .env File and Stop Leaking Secrets

By Vishnu Damwala

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:

PlatformWhere to set secrets
NetlifySite settings → Environment variables
VercelProject settings → Environment variables
RailwayProject → Variables
RenderService → Environment
AWSSecrets Manager or Parameter Store
Herokuheroku config:set KEY=value
DockerDocker secrets or compose env_file
KubernetesKubernetes 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

  • .env is in .gitignore before first commit
  • .env.example exists 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 .env file
  • Team knows to run cp .env.example .env on first setup

Containerizing your app? Learn How to Write a Production Dockerfile for Node.js and keep secrets out of Docker images too.