How to Use .env Files Safely: Best Practices for Secrets
What .env files are for, how to load them in Python, Node and Docker, how to keep them out of Git, and what to do when a secret leaks.
· 6 min read
Contents
- Why configuration belongs outside the code
- The format
- Rule 1: Never commit .env
- Rule 2: Commit a .env.example instead
- Rule 3: Validate configuration at startup
- Python (with pydantic-settings)
- Node.js
- Docker Compose
- Rule 4: Frontend "env" variables are public
- Rule 5: Production deserves better than a file
- Other ways secrets leak
- Scan for secrets automatically
- What to do if a secret leaks
- FAQ
- Is it safe to store secrets in environment variables?
- Should .env.example contain real values?
- Can I have different files per environment?
- Further reading
A .env file is a plain text file of KEY=value lines that holds an app's configuration — database URLs, API keys, feature flags — outside the code. It is one of the simplest and most widely used conventions in software, and also one of the most common sources of leaked secrets.
Here is how to use .env files properly, in development and production, and what to do when something goes wrong.
Why configuration belongs outside the code
The idea comes from the widely cited "twelve-factor app" methodology: configuration that varies between environments should live in environment variables, not in the code. That gives you:
- The same code running in development, staging and production with different settings.
- Secrets that never get committed to version control.
- Easy changes without a new release.
A .env file is just a convenient way to set those environment variables, mostly during local development.
The format
# .env
DATABASE_URL=postgresql://app:devpassword@localhost:5432/app
API_KEY=sk_test_1234567890
DEBUG=true
ALLOWED_HOSTS=localhost,127.0.0.1
GREETING="Hello, world"
Rules that most loaders follow:
- One
KEY=valueper line, no spaces around=. - Lines starting with
#are comments. - Quote values that contain spaces or special characters.
- Everything is a string.
DEBUG=truearrives in your code as"true", not a boolean — convert it explicitly.
Different tools handle edge cases (multiline values, variable expansion, export prefixes) slightly differently, so keep values simple.
Rule 1: Never commit .env
Add it to .gitignore before you create it:
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
echo "!.env.example" >> .gitignore
Starting a new project? Our .gitignore generator includes these rules in its "Secrets & .env files" template. Check that Git really ignores it:
git check-ignore -v .env
If .env was committed in the past, ignoring it now is not enough — it is still in the history. Remove it from tracking with git rm --cached .env, and treat every secret in it as leaked (see the last section).
Rule 2: Commit a .env.example instead
A .env.example file lists every variable the app needs, with safe placeholder values:
# .env.example — copy to .env and fill in real values
DATABASE_URL=postgresql://app:CHANGE_ME@localhost:5432/app
API_KEY=
DEBUG=false
ALLOWED_HOSTS=localhost
New developers run cp .env.example .env and know exactly what to fill in. Update it in the same commit whenever you add a variable. Mention it in your README.
Rule 3: Validate configuration at startup
Fail fast with a clear message if something is missing, instead of crashing later with a confusing error.
Python (with pydantic-settings)
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
database_url: str
api_key: str
debug: bool = False
allowed_hosts: list[str] = ["localhost"]
settings = Settings() # raises a validation error listing missing or invalid values
It reads real environment variables first and the .env file as a fallback, converts types ("true" → True), and validates them. Without extra libraries, python-dotenv plus os.environ["API_KEY"] (which raises KeyError if missing) also works.
Node.js
Node.js 20.6 and later can load a .env file natively:
node --env-file=.env server.js
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error("API_KEY is not set");
}
const debug = process.env.DEBUG === "true";
On older versions, use the dotenv package. Schema libraries such as Zod are a good way to validate and type the whole configuration at once.
Docker Compose
Compose reads a .env file in the project folder automatically for variable substitution in compose.yaml:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
To pass a whole file into a container's environment, use env_file:
services:
app:
image: myapp
env_file: .env
The :? syntax makes Compose stop with an error if the variable is missing. More in our Docker Compose guide.
Rule 4: Frontend "env" variables are public
Build tools for frontends inline certain variables into the JavaScript bundle that every visitor downloads. In Next.js that is anything prefixed NEXT_PUBLIC_; in Vite it is VITE_. Never put secrets in these. Anyone can read them with the browser's developer tools.
Keep secret keys on the server and have the frontend call your backend, which calls the third-party API.
Rule 5: Production deserves better than a file
.env files are great for local development. In production, prefer:
- Your platform's secret settings: most hosting platforms, CI systems and orchestrators let you set environment variables or secrets in their dashboard or CLI, encrypted at rest.
- A secrets manager for larger setups: cloud provider secret stores or tools like HashiCorp Vault, with access control and audit logs.
- Docker or Kubernetes secrets, which mount secrets as files instead of environment variables.
If you do use a .env file on a server, lock down its permissions:
chmod 600 .env
Other ways secrets leak
- Logs: printing the whole configuration or request headers at startup. Log the names of loaded variables, not their values.
- Error pages: debug modes that show environment variables. Turn debug off in production.
- Docker images: copying
.envinto an image withCOPY . .. Add.envto.dockerignore, and never pass secrets as build arguments that end up in image layers. - Screenshots and screen sharing: close the
.envtab before you present. - AI assistants and pastebins: do not paste real secrets into chats or online tools.
Scan for secrets automatically
Tools such as gitleaks and trufflehog scan your repository and history for things that look like keys. Run one in CI or as a pre-commit hook:
gitleaks detect --source .
GitHub also offers secret scanning and push protection for repositories, which blocks pushes containing known key formats.
What to do if a secret leaks
- Rotate it immediately: create a new key or password and revoke the old one. This is the step that actually protects you — assume the old one is already in someone's hands.
- Check for misuse: review the provider's logs and billing for unexpected activity.
- Remove it from the code and history if needed, with tools like
git filter-repo, then force-push and tell collaborators to re-clone. Do this after rotating, never instead of it. - Find out how it happened and add a guard: a
.gitignoreentry, a pre-commit scanner, a review step.
For undoing commits in general, see how to undo almost anything in Git.
FAQ
Is it safe to store secrets in environment variables?
It is the standard approach and far better than hard-coding them. Environment variables can still be exposed by debug output or process inspection on the same machine, which is why high-security setups use secret managers or files mounted with tight permissions.
Should .env.example contain real values?
Only non-secret defaults. Use placeholders such as CHANGE_ME or leave secret values empty.
Can I have different files per environment?
Yes — for example .env.development and .env.test for local use, which many frameworks load automatically. Keep production secrets out of files in the repository entirely.
Further reading
Keep reading
How to Secure a New Linux VPS: 10 Steps for the First Hour
A practical hardening checklist for a fresh Ubuntu or Debian server: a sudo user, SSH keys, firewall, automatic updates, fail2ban and backups.
Self-Hosting on a Raspberry Pi: What Works and How to Set It Up
What a Raspberry Pi can realistically self-host, the hardware that makes it reliable, a step-by-step Docker setup and how to reach it securely from anywhere.
Docker Compose for Beginners: Self-Host Your First App
Learn Docker Compose step by step: install it, write your first compose.yaml, persist data with volumes, connect services and keep everything updated.