DevPlainly
Self-Hosting

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
  1. Why configuration belongs outside the code
  2. The format
  3. Rule 1: Never commit .env
  4. Rule 2: Commit a .env.example instead
  5. Rule 3: Validate configuration at startup
  6. Python (with pydantic-settings)
  7. Node.js
  8. Docker Compose
  9. Rule 4: Frontend "env" variables are public
  10. Rule 5: Production deserves better than a file
  11. Other ways secrets leak
  12. Scan for secrets automatically
  13. What to do if a secret leaks
  14. FAQ
  15. Is it safe to store secrets in environment variables?
  16. Should .env.example contain real values?
  17. Can I have different files per environment?
  18. 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=value per line, no spaces around =.
  • Lines starting with # are comments.
  • Quote values that contain spaces or special characters.
  • Everything is a string. DEBUG=true arrives 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 .env into an image with COPY . .. Add .env to .dockerignore, and never pass secrets as build arguments that end up in image layers.
  • Screenshots and screen sharing: close the .env tab 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

  1. 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.
  2. Check for misuse: review the provider's logs and billing for unexpected activity.
  3. 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.
  4. Find out how it happened and add a guard: a .gitignore entry, 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