Skip to content
Go back

dotenv Files: The Mistakes That Leak Secrets

By SumGuy 4 min read
dotenv Files: The Mistakes That Leak Secrets

The Problem

You create .env with your database password. You add node_modules/ to .gitignore but forget .env.

Months later, someone clones the repo and finds credentials in git history. Too late.

Even if you delete the file now, it’s there forever. Any fork, any copy, any snapshot has it.

Dotenv itself isn’t the problem. It’s how you use it.

The Rule

Never commit .env files. Ever.

Commit .env.example:

Terminal window
# .env.example (in git)
DATABASE_URL=postgresql://user:password@localhost/mydb
API_KEY=your-api-key-here
DEBUG=false
JWT_SECRET=your-secret-key

Developers copy it:

Terminal window
cp .env.example .env

Then fill in their local values. .env is in .gitignore, so they can’t accidentally commit it.

Proper .gitignore

# Environment files (ALWAYS)
.env
.env.local
.env.development.local
.env.production.local
.env.test.local
.env.*.local
# Alternate naming
.env.secret
.env.*.secret

Cover all variations. Your team will create .env.production.local sometimes. Block it.

For Production

Never use .env files in production. Load secrets from environment variables:

Terminal window
# On the server
export DATABASE_URL="postgresql://prod-user:secure-password@prod-host/mydb"
export API_KEY="production-api-key"
# Run the app
node app.js

Or use a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.):

Terminal window
# app.js
const mysql = require('mysql');
const {SecretsManager} = require('aws-sdk');
const sm = new SecretsManager();
async function connectToDatabase() {
const secret = await sm.getSecretValue({SecretId: 'prod/db'}).promise();
const {username, password, host} = JSON.parse(secret.SecretString);
return mysql.createConnection({
host,
user: username,
password
});
}

Docker example:

FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Don't copy .env — inject at runtime
CMD ["node", "app.js"]

Run with secrets injected:

Terminal window
docker run \
-e DATABASE_URL="postgresql://..." \
-e API_KEY="..." \
my-app:latest

If You Already Leaked Secrets

It’s fixable but painful. You have to:

  1. Rotate the secret (issue new credentials)
  2. Rewrite git history (remove the leaked version)
  3. Force-push

Step 1: Rotate the Secret

Change your database password. Issue a new API key. Do this first, before history rewriting.

Step 2: Remove from Git History

Option A: Remove the file (cleanest):

Terminal window
git filter-repo --path .env --invert-paths

This rewrites history, removing .env from all commits.

Option B: Redact the secrets (keeps the file, blanks the values):

Terminal window
# Manually edit the file to blank out secrets
echo "" > .env
git add .env
git commit --amend
git filter-repo --path .env

Step 3: Force-Push

Warning: This rewrites history. Notify your team.

Terminal window
git push --force-with-lease origin main

All developers must pull the rewritten history:

Terminal window
git reset --hard origin/main

Best Practices by Environment

Local Development

Terminal window
# .env (in .gitignore)
DATABASE_URL=postgresql://dev:dev@localhost/mydb
API_KEY=dev-key-12345
DEBUG=true

Run the app:

Terminal window
node app.js

Staging

Load from environment:

Terminal window
# GitLab CI
variables:
DATABASE_URL: $STAGING_DATABASE_URL # Set in CI/CD variables
API_KEY: $STAGING_API_KEY
script:
- npm start

Or Docker:

Terminal window
docker run \
-e DATABASE_URL="$STAGING_DATABASE_URL" \
-e API_KEY="$STAGING_API_KEY" \
staging-app:latest

Production

Same approach, with stricter controls:

Terminal window
# Kubernetes secret
kubectl create secret generic app-secrets \
--from-literal=DATABASE_URL="postgresql://..." \
--from-literal=API_KEY="..."
# Pod mounts it
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: DATABASE_URL

Testing with .env

For tests, use a .env.test:

Terminal window
# .env.test (in git, safe values)
DATABASE_URL=postgresql://test:test@localhost/test_db
API_KEY=test-key
DEBUG=false

But don’t use .env for tests. Instead:

// test setup
process.env.DATABASE_URL = 'postgresql://test:test@localhost/test_db';
process.env.API_KEY = 'test-key';
// Now run tests

Or use a test library:

jest.config.js
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/test-setup.js']
};
// test-setup.js
process.env.DATABASE_URL = 'postgresql://test:test@localhost/test_db';
process.env.API_KEY = 'test-key';

This way, .env.test doesn’t exist and tests are explicit about what they need.

Auditing: Check Your Repo

Scan your git history for secrets:

Terminal window
# Using truffleHog
pip install truffleHog
truffleHog git file:///path/to/repo
# Or using git grep
git log -p -S 'password' | head -100

Check if anything’s been leaked. If yes, rotate secrets immediately and rewrite history.

Common Mistakes

Mistake 1: Committing .env “just for now.”

Don’t. It’s never “just for now.”

Mistake 2: .env in Docker image.

# Bad
COPY .env .

Secrets are baked into the image. Anyone with the image has the secrets.

Mistake 3: Printing environment variables in logs.

console.log(process.env); // Leaks all secrets

Be selective. Log only what you need to debug.

The Checklist

Do all of this, and you’re safe.

Skipping even one? You’re one forked repo away from a security incident.

Take 30 minutes. Set it up right. Never leak secrets again.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Browser GPU Acceleration on Linux in 2026
Next Post
Alert Fatigue: Why Your Alerts Are Meaningless

Related Posts