The Complete .env File Format Guide
.env looks like it has no rules at all - just KEY=value, one per line. It has more rules than that, and most of them only matter the day you hit an edge case.
Check Your Own File First
Paste a .env file below and the validator will flag duplicate keys, invalid key names, unquoted values containing whitespace, and unescaped shell-special characters — the four issues that cause the most confusing "why is my config wrong" debugging sessions. Everything runs in your browser; nothing is uploaded.
Paste a .env file to check for duplicate keys, invalid key names, unquoted values with whitespace, and unescaped shell-special characters.
Privacy note: parsing and validation run entirely client-side. Your .env contents are never sent to a server.
The Basic Shape
A .env file is a sequence of lines, each one of three things: a blank line, a comment line starting with #, or a KEY=value entry. There is no schema, no required order, and no file extension requirement beyond convention — .env is a filename, not a format registered anywhere official. What behavior it has comes from the parsers that read it, and the closest thing to a de facto specification is the Node.js dotenv package, whose parsing rules nearly every other language's .env library has converged on.
Keys conventionally use SCREAMING_SNAKE_CASE — uppercase letters, digits, and underscores, starting with a letter or underscore. This isn't enforced by the file format itself, but it matters in practice: these files are frequently sourced directly by a shell or exported into a process environment, and shell variable names are restricted to exactly this character set. A key like 1INVALID_KEY or my-key will silently break in some consumers and work in others, which is worse than failing consistently.
Quoting Rules
Unquoted, single-quoted, and double-quoted values behave differently, and the differences aren't cosmetic:
- Unquoted (
KEY=value) — trailing whitespace is trimmed, and a#only starts an inline comment when it's preceded by whitespace.URL=http://x#fragmentkeeps the whole string as the value because the#has no space before it;URL=http://x #fragmenttruncates the value at the space and treats the rest as a comment. This one rule explains a surprising number of "my URL got cut off" bug reports. - Single-quoted (
KEY='value') — completely literal. No escape sequences are processed and the value cannot span multiple lines. Use this when a value legitimately contains characters you don't want interpreted, like a password with a literal backslash. - Double-quoted (
KEY="value") — the only style that processes escapes:\n,\t,\r,\", and\\all expand to their special-character equivalents, and the value is allowed to span multiple physical lines until the closing quote is found.
Multiline Values
Because double-quoted values can span lines, this is valid and produces a single value containing a real newline character:
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA... -----END RSA PRIVATE KEY-----"
This is the standard way to embed a PEM-formatted key or any other multiline secret directly in a .env file without a separate file and a _FILE pointer variable. The alternative — replacing real newlines with the literal two-character sequence \n on a single line — also works with double quotes, since \n is expanded on load; which style you pick is mostly about whether the value is easier to read broken across lines or kept on one.
Comments
A line whose first non-whitespace character is # is a standalone comment and is ignored entirely. Comments are commonly used to group related variables and to document what a value should look like without committing a real example:
# Database DATABASE_URL=postgres://localhost:5432/app # Stripe (test mode keys only in local .env) STRIPE_SECRET_KEY=sk_test_...
Inline trailing comments after a value are also supported, subject to the quoting rule above for unquoted values: a space before the # is what turns it into a comment rather than part of the value.
The export Prefix
Many .env parsers, including dotenv, accept an optional leading export keyword on any entry line:
export DATABASE_URL=postgres://localhost:5432/app DATABASE_URL=postgres://localhost:5432/app
Both lines above parse to the identical key and value — the export keyword is stripped and ignored by .env parsers. It exists purely for a second use case: the same file can be sourced directly by a POSIX shell (source .env or . .env), where export is required to push the variable into the environment rather than leaving it as a shell-local variable. Prefixing every line with export is what lets one file serve both purposes; without it, a shell-sourced .env sets variables that only the current shell session can see, not any child process it launches.
How Runtimes Actually Load It
Node.js
The dotenv package is the reference implementation most other libraries mirror. The idiomatic pattern loads it as early as possible, typically as the first line of your entry point:
require('dotenv').config()
// or, ESM: import 'dotenv/config'
console.log(process.env.DATABASE_URL)Crucially, dotenv does not overwrite variables that are already set in process.env — if your shell or orchestrator already exported DATABASE_URL, the .env file's value is ignored for that key. This makes .env a set of defaults, not an override mechanism, which is usually what you want in production where real environment variables should win.
Docker
Docker has native support via --env-file:
docker run --env-file .env myimage
Docker's own parser is deliberately simpler than dotenv's: it supports KEY=value and comment lines, but does not process quotes the same way — a value like KEY="value" passed through --env-file keeps the literal quote characters as part of the value, which surprises people who assume Docker and Node.js parse identically. Compose files (docker-compose.yml) reference the same env_file: mechanism and share this simpler parsing behavior.
Python
python-dotenv is the standard library equivalent, following the same quoting and escaping rules described above:
from dotenv import load_dotenv
import os
load_dotenv()
database_url = os.environ.get("DATABASE_URL")Frameworks built on top — Django via django-environ, FastAPI via Pydantic's BaseSettings — add typed parsing and validation but rely on the same underlying file format described here.
A Word on What Not to Put in a .env File
The format has no concept of encryption, access control, or audit logging — a .env file is plain text, full stop. That's appropriate for local development defaults and non-sensitive configuration, but production secrets belong in a secret manager your platform can rotate and audit. See managing .env files across environments for where the line should be, and .env security best practices for what to do if one leaks anyway.
Once your file is in good shape, try the full .env Toolkit to compare it against another environment, merge two files, or generate a safe .env.example to commit alongside it.
Further Reading
- dotenv (npm) documentation
The reference Node.js implementation whose parsing rules most other .env libraries follow.
- The Twelve-Factor App: Config
The methodology that popularized storing configuration in the environment, separate from code.
- python-dotenv documentation
The Python equivalent of dotenv, with the same quoting and escaping semantics.
- Docker docs: Env file reference
Docker's own --env-file parsing rules, which differ from dotenv's quote handling.