TL;DR
A .env file is a plain-text file that stores environment variables — configuration values like database URLs, API keys, and feature flags — outside your source code. Frameworks load these variables at runtime so the same code can run in development, staging, and production without modification.
Why environment variables exist
Twelve-Factor App principle III says: store config in the environment. The reason: config changes between deploys, but code shouldn't. A .env file is the local-dev-friendly way to follow that rule — every framework can read from process.env (or the equivalent), and every host (Vercel, Railway, Fly, AWS, GCP, Azure) can inject those same variables without shipping them in your build artifacts.
The format
A .env file is just newline-separated KEY=VALUE pairs:
# Example .env
NODE_ENV=production
DATABASE_URL=postgres://user:pass@localhost:5432/app
JWT_SECRET=a-very-long-random-string
DEBUG=trueA few rules every runtime agrees on:
- Keys are case-sensitive and conventionally
UPPER_SNAKE_CASE. - Values are strings by default — type coercion (to number, boolean) is the framework's job.
- Lines starting with
#are comments. - Empty lines are ignored.
- Quotes around values are optional, but required when the value contains spaces or special characters.
How different stacks load it
Node.js
Load with the dotenv package (or Node 20+'s built-in --env-file flag). Access via process.env.KEY.
// package.json
"scripts": { "start": "node --env-file=.env server.js" }
// server.js
console.log(process.env.DATABASE_URL);Next.js
Reads .env.local automatically. Variables prefixed with NEXT_PUBLIC_ are exposed to the browser; everything else is server-only. Load order: .env → .env.local → .env.development → .env.production, with later files overriding earlier ones.
Python / Django
# settings.py
from dotenv import load_dotenv
load_dotenv()
DATABASE_URL = os.environ["DATABASE_URL"]Ruby / Rails
Rails uses dotenv-rails. The gem auto-loads during boot; values land in ENV["KEY"].
Laravel
Laravel uses the vlucas/phpdotenv package under the hood. Access via env("KEY"), but only inside config files — never at runtime, because config is cached in production.
.env, .env.local, .env.example — what's the difference?
| File | Purpose | Commit to git? |
|---|---|---|
.env | Base defaults (sometimes checked in) | Sometimes |
.env.local | Your machine's overrides | Never |
.env.example | Keys without values — structure only | Always |
.env.production | Prod-specific values | Never |
.env.test | Test-only values | Sometimes |
Generate a safe .env.example from any .env if you haven't committed one yet.
Why .env files break production
Because they're strings. And because they're usually edited by hand. The most common failures:
- Missing key:
process.env.API_KEYisundefined→ NullPointerException at runtime. - Duplicate key: dotenv keeps the first; most other parsers keep the last. Silent divergence between local and prod.
- Bad quotes:
SECRET="abcswallows the rest of the file. - Committed secret: a
.envaccidentally checked in → secrets need rotation.
You can catch most of these with the ENV validator and the leak checker — both run in the browser.
What comes next
Once you've got a valid .env, the next question is how to manage it across machines and environments. Our .env best practices guide covers the 10 rules that matter most.