Skip to main content
Loading time...

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#fragment keeps the whole string as the value because the # has no space before it; URL=http://x #fragment truncates 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