What Is a .env File and Why Does It Exist?

A .env file is a plain text file that stores environment-specific configuration as key-value pairs. It was popularised by the twelve-factor app methodology, which argues that configuration that changes between environments (development, staging, production) should be stored in environment variables, not hardcoded into source code.

# Database
DATABASE_URL=postgres://user:password@localhost:5432/mydb
DATABASE_POOL_SIZE=10

# Third-party services
STRIPE_SECRET_KEY=sk_live_abc123xyz
SENDGRID_API_KEY=SG.xxxxxxx

# Application settings
APP_ENV=production
JWT_SECRET=super-secret-signing-key-here
LOG_LEVEL=info

Libraries like dotenv (Node.js), python-dotenv (Python), and godotenv (Go) load this file at startup and inject the values into the process environment. The problem isn't the format — it's how developers handle these files around version control.

Rule #1: .env Should Always Be in .gitignore

This is non-negotiable. Your .env file should be listed in .gitignore from day one. Not day two. Not after you've committed it once and removed it.

# .gitignore
.env
.env.local
.env.*.local
*.env

Important: removing a .env file from a Git commit does not remove it from history. If you've ever committed credentials, rotate them immediately — don't just delete the file. To check whether a .env file has ever appeared in your Git history:

git log --all --full-history -- .env
git log --all -p -- .env | grep "^+" | grep -i "secret\|key\|password\|token"

If you find historical commits, rotate all credentials and use git-filter-repo to scrub the history if needed.

Rule #2: Commit .env.example — Not .env

The right pattern is to commit a .env.example file that lists every variable your application needs, with placeholder values and comments. New developers clone the repo, copy .env.example to .env, and fill in real values.

# .env.example — SAFE TO COMMIT — never put real secrets here
# Copy this file to .env and fill in the values for your environment

# Required: PostgreSQL connection string
DATABASE_URL=postgres://user:password@host:5432/dbname

# Required: Stripe API key (get from https://dashboard.stripe.com/apikeys)
STRIPE_SECRET_KEY=sk_live_REPLACE_ME

# Optional: Log level (debug|info|warn|error) — defaults to info
LOG_LEVEL=info

# Required in production: JWT signing secret (min 32 characters)
JWT_SECRET=REPLACE_WITH_RANDOM_STRING_MIN_32_CHARS

This file documents your config surface area, makes onboarding faster, and stays safely in version control. Your .env with real secrets stays local and untracked.

Rule #3: Validate Your .env Before Running

Missing or malformed environment variables are a surprisingly common source of production incidents. The app starts, doesn't fail immediately, and then crashes at runtime when it first tries to use the missing variable — often under load.

Node.js with Zod:

import { z } from 'zod';
import dotenv from 'dotenv';

dotenv.config();

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  JWT_SECRET: z.string().min(32),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
  PORT: z.coerce.number().default(3000),
});

const env = envSchema.parse(process.env);
export default env;

If any variable is missing or invalid, this throws a clear error at startup rather than a cryptic runtime failure later.

Python with pydantic-settings:

from pydantic_settings import BaseSettings
from pydantic import PostgresDsn, SecretStr

class Settings(BaseSettings):
    database_url: PostgresDsn
    stripe_secret_key: SecretStr
    jwt_secret: SecretStr
    log_level: str = "info"

    class Config:
        env_file = ".env"

settings = Settings()  # Raises ValidationError with clear message if anything is wrong

Rule #4: Never Log Environment Variables

This ties directly to log hygiene. The database URL pattern postgres://user:password@host:port/db embeds the password in plain text. Logging it sends it to your log aggregation platform — Datadog, Splunk, ELK. Do not do this:

// ❌ Never do this
console.log("Loaded config:", process.env);
console.log(`Connecting to: ${process.env.DATABASE_URL}`); // URL contains password

Even a debug log level can leak credentials if debug logs are ever enabled in a production-adjacent environment. See our Log Masker guide for patterns to detect and redact secrets at the shipper level.

Rule #5: Use a Secret Manager in Production

In production, .env files are an anti-pattern. Hard-coding secrets into a file on a server — even securely — means they exist somewhere on disk in plaintext and need to be manually rotated. The production-grade alternative is a dedicated secret manager:

PlatformSecret ManagerCost
AWSAWS Secrets Manager~$0.40/secret/month
GCPGoogle Secret Manager~$0.06/10k operations
AzureAzure Key Vault~$0.03/10k operations
Self-hostedHashiCorp VaultFree (OSS)
Any cloudDopplerFree tier available

For Kubernetes, the External Secrets Operator is the standard pattern for pulling secrets from a vault into Kubernetes Secret objects without hardcoding them in manifests.

📋

DevOpsArsenal .env File Parser & Validator

Paste your .env file content, and the tool parses it, highlights syntax issues, and shows you exactly what each variable will resolve to after parsing — so you catch issues before your application does. Runs entirely in your browser; nothing is sent to a server.

Try .env Validator Free →

A .env Security Checklist

Before deploying or sharing any environment configuration, run through this list:

  • .env is in .gitignore and has never been committed to version control
  • .env.example is committed with placeholder values and comments
  • All secrets are validated at application startup
  • No environment variables are logged anywhere in the codebase
  • Production uses a secret manager, not a .env file on disk
  • Secrets are rotated after every team member departure
  • Development secrets differ from staging secrets, which differ from production
  • CI/CD secrets are stored in the pipeline's secret store (GitHub Actions secrets, GitLab CI variables)

Frequently Asked Questions

Is it safe to commit .env.example to version control?
Yes — as long as it contains no real secrets. Use clearly fake placeholders such as REPLACE_ME or your-api-key-here, and add comments explaining where to get real values. .env.example is documentation, not configuration.
My .env was committed once — do I just delete it?
No. Deleting the file in a new commit leaves it in Git history. You must rotate all credentials that were exposed immediately, then optionally use git-filter-repo to remove the file from history. If the repository was ever public, even briefly, assume the credentials were harvested by automated scanners.
Should I use a .env file in Docker containers?
Avoid it in production. Docker has native environment variable support via --env-file, but these write values to the container environment where they are visible via docker inspect. The better approach in production is to use Docker secrets for Swarm or Kubernetes secrets sourced from a vault.
What's the difference between environment variables and secrets?
Environment variables are the mechanism. Secrets are a category of data — credentials, API keys, private keys — that require additional protection. Not all environment variables are secrets (LOG_LEVEL=info is not sensitive), but all secrets in a twelve-factor app are stored as environment variables.
The .env file is one of the most useful patterns in modern application development — and one of the most commonly mishandled. The rules are simple: never commit secrets, always validate on startup, never log env values, and move to a proper secret manager before you go to production. Use the DevOpsArsenal .env Validator to catch syntax issues before they cause runtime failures, and keep the rest of these practices as standing team hygiene, not one-time fixes.