Environment variables and secrets
Environment variables and secrets
.env files and environment variables show up early for newcomers. This article covers their origins, behavior, and the tools used for secrets management — based on facts.
1. Where env vars and .env come from
An environment variable is a key-value pair a process reads from the operating system. On Unix it's accessed via getenv(3), on Windows via GetEnvironmentVariable. The shell's export FOO=bar (bash) or $env:FOO = "bar" (PowerShell) hands them to child processes.
.env is the convention of writing those key-value pairs into a text file and letting a library inject them into the process environment. There is no formally defined standard. The de facto standards split into two:
- dotenv (Node.js, motdotla/dotenv, 2013) — Started by Brandon Keepers, taken over by Scott Motte. Reads
KEY=valuelines and fillsprocess.env. BSD-2. - python-dotenv (2014) — Started by Saurabh Kumar. Same behavior for Python's
os.environ.
The Twelve-Factor App methodology (Adam Wiggins, Heroku, 2011) cemented the pattern when its III. Config section said "store config in the environment".
Why environment variables won out:
- Language- and OS-agnostic standard.
- Low chance of accidentally landing in a code repo.
- Managed independently per deployment.
2. How it works
How .env ends up in the environment at runtime:
- A library (
dotenv,python-dotenv, Vite, Next.js, etc.) reads the file at startup. - Parses it line by line (handling quotes, whitespace, comments) and sets keys in
process.envoros.environ. - If the same key already exists in the OS environment, it is usually not overwritten (depends on the library and options).
Cases where the runtime auto-loads:
- Next.js — Auto-loads
.env.local,.env.development,.env.production. TheNEXT_PUBLIC_prefix is bundled to the client; everything else stays server-side. - Vite — Auto-loads
.env, exposes only theVITE_prefix to the client. - Docker Compose — Injects via
env_file:or--env-file.
3. Per-environment split conventions
| File | Intent |
|---|---|
.env |
Common values across environments. Whether to commit it is team policy. |
.env.local |
Per-developer machine. Usually gitignored. |
.env.development / .env.production |
Per-environment defaults. |
.env.example |
Template with key names only. Tells new developers which vars to fill. |
4. Secrets management tools
A common opinion: real secrets (external API keys, DB passwords) should be managed differently from build settings (public URLs, feature flags).
- HashiCorp Vault (2015) — Self-hostable secrets manager. Dynamic password issuing, short-lived tokens.
- AWS Secrets Manager, GCP Secret Manager, Azure Key Vault — Cloud-managed equivalents.
- SOPS (Mozilla, 2017 OSS) — Encrypts files with KMS / age / PGP keys and commits them straight to git. The structure remains diff-friendly, which fits GitOps well.
- 1Password CLI (
op) — Pulls secrets from a 1Password vault into shell environment variables. - Doppler, Infisical — SaaS secrets managers. CLIs replace
.env. - GitHub Actions Secrets — CI-only secrets.
Some setups run .env in production directly; others keep .env for development only and switch to a secrets manager in production. Both are common. The right side depends on team size, regulatory needs, and operations capacity.
5. Common shapes
.env.example (committed):
DATABASE_URL=postgres://user:pass@localhost:5432/app
SMTP_HOST=
SMTP_USER=
SMTP_PASS=
JWT_SECRET=replace-me
Node side:
import 'dotenv/config';
console.log(process.env.DATABASE_URL);
Python side:
from dotenv import load_dotenv
import os
load_dotenv()
db_url = os.environ["DATABASE_URL"]
Direct shell:
# macOS · Linux
export FOO=bar
echo $FOO
unset FOO
# Windows PowerShell
$env:FOO = "bar"
echo $env:FOO
Remove-Item Env:FOO
6. Common pitfalls
Accidental .env commit — secrets land in git history. The safest response is key rotation. Tools like git filter-repo rewrite history, but anyone who has already cloned still holds the leak.
Key collisions — when the OS environment already has a variable of the same name, libraries may ignore the .env value. Check options like dotenv's override ahead of time.
Newlines and quotes — putting multi-line PEM keys into .env runs into different quote and \n handling per library. Follow the official docs example.
Client-bundle exposure — frontend build tools claim to expose only certain prefixes (NEXT_PUBLIC_, VITE_), but anything that lands in the bundle reaches users in plaintext. Real secrets do not belong in client variables.
"Should .env be committed?" — Team policy. With plaintext secrets inside, usually no. If the team intentionally restricts the file to plaintext config only, then yes. Either way, write the policy down to reduce mishaps.
Closing thoughts
Environment variables are a simple, strong standard codified by the Twelve-Factor App's III. Config section. Keep real secrets out of git, and bringing in a secrets manager (Vault, SOPS, 1Password) raises operational safety a level. Real secrets must never reach the client bundle (the prefix rules are only the build tool's helping hand).
Next
- version-managers
- git-workflow
References include the dotenv (Node.js) GitHub, python-dotenv docs, HashiCorp Vault docs, Mozilla SOPS, The Twelve-Factor App III. Config, OWASP Secrets Management Cheat Sheet, and the GitGuardian State of Secrets Sprawl.