Why .env files exist
The .env file pattern, popularised by the dotenv library, solved a real problem: keeping configuration values out of source code so developers could work with different settings across environments. Load secrets from a file, read them via process.env, and keep the file out of version control with .gitignore.
For local development, this works fine. The mistake is treating the same pattern as a production security strategy. A .env file on a production server is a plaintext file sitting on disk, readable by any process with file system access, exposed by misconfigured web servers, and persisted in Docker image layers and deployment artifacts.
Five ways .env files leak secrets
1. Committed to git
The most common leak. A developer creates a .env file, forgets to add it to .gitignore (or adds the wrong filename), and pushes it. Even if they delete it in the next commit, the secret lives permanently in git history. GitHub's own research shows that millions of secrets are pushed to public repositories every year. We routinely recover production credentials from git history during penetration tests.
2. Docker image layers
A common Dockerfile pattern: COPY .env . in an early stage, then RUN rm .env later. The deletion only creates a new layer — the original layer with the .env file intact is still part of the image. Anyone who pulls the image can extract every layer and read the file. Use docker history or dive to verify this yourself.
3. Exposed by web servers
If your .env file sits in a directory served by Nginx or Apache, and the server isn't configured to block dotfiles, it's accessible via https://yourapp.com/.env. We test for this in every web application pentest. It's a trivial check, and it succeeds more often than you'd expect.
4. Error pages and debug output
Frameworks like Django (with DEBUG=True), Laravel (with APP_DEBUG=true), and Express (with verbose error handlers) can dump environment variables in error responses. A single unhandled exception can display your database password, API keys, and JWT secrets to anyone who triggers the right error. We cover framework-specific fixes in our Django security guide and Laravel security guide.
5. Backup and deployment artifacts
Server backups, rsync copies, CI/CD artifacts, and scp transfers all pick up .env files. If your deployment process copies the project directory to a server, the .env file travels with it — often to locations with broader file permissions than intended.
.env accessible via HTTP on a payment platform
During a routine assessment of a Nigerian fintech's staging environment, we accessed the .env file directly via the web server. It contained production Paystack secret keys, database credentials, and the JWT signing secret. The staging server had been set up by copying the production configuration.
The .env.example pattern
Every project should include a .env.example file — committed to git — that documents every required environment variable with placeholder values. This serves as documentation for new developers without exposing real secrets.
The format is simple:
# .env.example — commit this file
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxx
JWT_SECRET=replace-with-a-strong-random-value
REDIS_URL=redis://localhost:6379 Pair this with a startup check that validates all required variables are present and non-empty. Fail fast in development, fail loud in production. Never silently fall back to defaults for security-critical values.
Production secrets management
In production, secrets should come from infrastructure, not files. The hierarchy of approaches, from simplest to most robust:
Platform environment variables
Vercel, Heroku, Render, Railway, and Fly.io all provide environment variable configuration through their dashboards or CLIs. These are injected into your runtime process without touching the filesystem. This is the minimum viable approach. For details on each option, see where to store API keys and secrets.
CI/CD secret injection
GitHub Actions Secrets, GitLab CI Variables, and CircleCI Contexts store encrypted values that are injected at build or deploy time. Scope secrets per environment. Never use the same credentials across staging and production.
Secret management services
AWS Secrets Manager, Google Secret Manager, HashiCorp Vault — these provide encryption at rest, access policies, audit trails, and automatic rotation. If you're processing payments or handling KYC/BVN data, a managed secret service isn't optional — it's a compliance expectation under NDPA/NDPR and PCI DSS.
Want to know if your secrets are leaking through .env files, Docker layers, or git history?
Get a secrets auditPractical hardening checklist
- Add
.env,.env.local,.env.production, and.env.*.localto.gitignoreimmediately - Run
git log --all --full-history -- .envto check if.envwas ever committed. If it was, rotate every secret in it - Use
trufflehogorgitleaksto scan your entire git history for leaked credentials - Configure your web server to block all dotfile requests:
location ~ /\. { deny all; }in Nginx - Use multi-stage Docker builds with
--mount=type=secretinstead ofCOPY .env - Set
DEBUG=False/APP_DEBUG=falsein every production deployment - Validate required environment variables at application startup — fail immediately if any are missing
- Maintain a
.env.examplewith every required variable documented
The .env file is for development, not production
In development, .env files are fine. In production, secrets should come from your platform's environment configuration, a CI/CD pipeline, or a dedicated secret manager. The file should never exist on a production server, in a Docker image, or in git history.
Related reading
Blog: Where to store API keys · Can hackers see your source code? · Cloud security checklist
Guides: Fintech security checklist · PCI DSS compliance · NDPA/NDPR compliance
Services: Penetration testing · Secure architecture review · Vulnerability assessment