The .env file format has no formal specification. The env file syntax that ships in motdotla/dotenv 17.x for Node.js diverges from theskumar/python-dotenv 1.0, Ruby bkeepers/dotenv 3.2, joho/godotenv 1.5, and the parser inside Docker Compose v2 in small ways that look like bugs but leak passwords or truncate URLs in practice. motdotla/dotenv v15.0.0 (Jan 31, 2022) made # the inline-comment marker on unquoted values by default — the upgrade caught teams pinned to v14 with a # in a password.
This reference documents the real .env file format rules — quoting, comments, multi-line values, variable expansion, and the export prefix — with explicit notes on where each implementation diverges. Use the env validator to check your files against these rules instantly.
What is the basic KEY=VALUE syntax?
Every .env file is a plain-text file where each line defines one environment variable as a KEY=VALUE pair. Keys are uppercase by convention and may contain letters, digits, and underscores. The value starts immediately after the first = character.
DATABASE_HOST=localhost
PORT=3000
API_KEY=sk_live_abc123Blank lines are ignored. A line that contains only whitespace is also ignored.
How do quoting rules work?
Values can be unquoted, single-quoted, or double-quoted. The quoting style determines how the parser handles special characters, whitespace, and escape sequences.
Unquoted values
The value is read as-is from after the = until end of line. Leading and trailing whitespace is trimmed by most parsers. Inline comments with # may or may not be stripped depending on the implementation.
APP_NAME=my-app
DEBUG=trueDouble-quoted values
Double quotes preserve inner whitespace and allow escape sequences. Most parsers interpret \n as a newline, \t as a tab, and \\ as a literal backslash. Variable expansion with ${VAR} is active inside double quotes.
GREETING="Hello, World!"
MESSAGE="Line one\nLine two"
PASSWORD="p@ss#word with spaces"Single-quoted values
Single quotes treat everything as a literal string. No escape sequences are processed and no variable expansion occurs. Use single quotes when your value contains backslashes, dollar signs, or other special characters that must remain verbatim.
REGEX='\d+\.\d+'
TEMPLATE='Hello $USER, welcome!'
RAW_PATH='C:\Users\admin'How do I add comments to a .env file?
Put a # at the start of a line — every major parser (Node.js motdotla/dotenv 17.x, python-dotenv 1.0, Ruby bkeepers/dotenv 3.2, Go joho/godotenv 1.5, Docker Compose v2) ignores the line. Leading whitespace before the # is fine. Blank lines are also ignored.
# Database configuration
DB_HOST=localhost
# Indented comments are also valid
DB_PORT=5432The trickier question is inline comments — does KEY=value # comment set the value to value or to value # comment? Behavior diverges by library and version, and the divergence has shipped real bugs.
Node.js dotenv (motdotla/dotenv)
Up to v14.3.2, inline comments after unquoted values were preserved verbatim — the value value # comment ended up as "value # comment". v15.0.0 (released Jan 31, 2022) flipped the default and now strips the comment from unquoted values. Inside quoted values # is always literal. This change broke teams that had # characters in passwords pinned to v14.
// .env
TOKEN=abc#123 # API token
QUOTED_TOKEN="abc#123" # API token
// Node.js with dotenv@17 (or @15+)
require('dotenv').config();
process.env.TOKEN; // "abc"
process.env.QUOTED_TOKEN; // "abc#123"Python python-dotenv (theskumar/python-dotenv)
python-dotenv 1.0 strips inline comments from unquoted values when there is whitespace before the # — without that whitespace, # is treated as part of the value. Inside quoted values # is always literal. The whitespace requirement is the inverse of what most readers assume from the Node.js behavior, and it bites when migrating .env files between ecosystems.
# .env
TOKEN=abc#123 # API token
NO_SPACE=abc#123# API token
QUOTED_TOKEN="abc#123" # API token
# Python with python-dotenv@1.0
from dotenv import dotenv_values
dotenv_values('.env')
# {'TOKEN': 'abc#123', 'NO_SPACE': 'abc#123# API token',
# 'QUOTED_TOKEN': 'abc#123'}Docker Compose v2
Compose's env_file parser (compose-spec/compose-go) splits on the literal two characters " #" — same whitespace requirement as python-dotenv. Without a space before the #, the hash stays in the value. Quoted values keep # literal. Same portability rule applies: quote anything with #.
# .env consumed by env_file in docker-compose.yml
TOKEN=abc#123 # API token # → TOKEN=abc#123
NO_SPACE=abc#123# API token # → NO_SPACE=abc#123# API token
QUOTED_TOKEN="abc#123" # token # → QUOTED_TOKEN=abc#123The portable rule: never rely on inline comments for any value containing #, and quote the value whenever it might. URLs with fragment identifiers are the most common landmine — see the dotenv-not-loading guide for the full failure-mode list.
# Dangerous — may be truncated to "https://example.com/path"
URL=https://example.com/path#anchor
# Safe — quotes preserve the full value
URL="https://example.com/path#anchor"How do I write multi-line values in a .env file?
Two approaches. The first works in every parser; the second is parser- and version-dependent. The most common real-world need is shipping an RSA or EC private key (TLS certificates, JWT signing, GitHub App credentials) without flattening it onto one line.
Escape sequences inside double quotes (universal)
Embed the literal two characters \n inside double quotes. The parser converts them to a real newline at parse time. Works in Node.js motdotla/dotenv, python-dotenv, Ruby dotenv, and Go godotenv.
MULTILINE="line one\nline two\nline three"Real newlines spanning multiple lines
Open a double quote, drop in real newlines, close the quote on a later line. Behavior by parser:
- Node.js motdotla/dotenv — supported since v15.0.0 (Feb 2022). Earlier versions silently truncated at the first newline.
- python-dotenv — supported since 0.10.0 (2018).
- Ruby bkeepers/dotenv — supported in all current 2.x and 3.x releases.
- Go joho/godotenv — supported since v1.5.0 (Feb 4, 2023); v1.5.1 the next day fixed early parser regressions. v1.4.0 (Sep 2021) did not support real newlines.
- Docker Compose v2 — not supported in
env_file. Compose throws anunexpected character "..." in variable nameerror. Use theenvironment:block in your YAML, or base64-encode the value and decode at container start. See the Docker Compose env variables guide for the patterns.
PRIVATE_KEY="-----BEGIN RSA KEY-----
MIIBogIBAAJBALRiMLAH...
-----END RSA KEY-----"If you have to support Docker Compose and a Node.js or Python service from the same .env file, use the \n-escape form — it survives every parser. For storage and rotation considerations of multi-line secrets, see .env vars security best practices.
How does variable expansion work?
Variable interpolation lets you reference other variables defined earlier in the same file or already present in the environment. Both $VAR and ${VAR} syntax are supported.
BASE_URL=https://api.example.com
# Both forms reference BASE_URL
API_V1=$BASE_URL/v1
API_V2=${BASE_URL}/v2Variable expansion is disabled inside single quotes. This is consistent across all major implementations. It is enabled in double quotes and unquoted values in Node.js dotenv (with dotenv-expand), Python python-dotenv, and Go godotenv. Ruby dotenv enables it by default in all quoting styles except single quotes.
What about empty and missing values?
There is an important distinction between an empty value and an absent variable.
# Empty string — the variable exists but has no content
DB_PASSWORD=
DB_NAME=""
DB_HOST=''
# These three all set the variable to an empty string ""If a key is not present in the .env file at all, most dotenv libraries will not set it, and the variable will be undefined (Node.js) or None (Python) unless it was already defined in the system environment.
How are spaces and the = sign handled?
The treatment of whitespace around the = sign differs between implementations. Most dotenv libraries do not allow spaces around the equals sign.
# Correct — no spaces around =
DATABASE_URL=postgres://localhost/mydb
# Risky — spaces around = may cause parse errors or unexpected keys
DATABASE_URL = postgres://localhost/mydbFor unquoted values, most parsers trim trailing whitespace. Leading whitespace after = is also typically trimmed. However, inside quotes, all whitespace is preserved exactly as written. The safest practice is to never rely on trimming — quote values that contain meaningful whitespace.
How do implementations differ across languages?
Because there is no formal specification, each dotenv library makes its own parsing decisions. The following table summarizes the key differences.
| Feature | Node.js dotenv | Python python-dotenv | Ruby dotenv | Go godotenv | Docker Compose |
|---|---|---|---|---|---|
| Inline comments | Yes (unquoted) | Yes (unquoted) | Yes (unquoted) | Yes (unquoted) | Yes (unquoted) |
| Variable expansion | Via dotenv-expand | Built-in | Built-in | Built-in | Built-in |
| Multiline (real newlines) | Yes (v15+) | Yes | Yes | Yes | No |
| export prefix | Ignored | Ignored | Ignored | Ignored | Not supported |
| Spaces around = | Trimmed | Trimmed | Trimmed | Trimmed | Not allowed |
| Overwrite existing env | No (default) | No (default) | No (default) | No (default) | Yes |
What does the export prefix do?
Some .env files use export KEY=VALUE so the file can be sourced directly in a shell session. Most dotenv parsers silently strip the export prefix and treat the line normally. Docker Compose is the notable exception — it does not recognize the prefix and will either error or treat the entire string as the key.
# Works in shell (source .env) and most dotenv parsers
export DATABASE_URL=postgres://localhost/mydb
export NODE_ENV=production
# Docker Compose: remove the export prefix
DATABASE_URL=postgres://localhost/mydbWhat are the most common gotchas?
- BOM characters — Some editors (particularly on Windows) insert a Unicode byte order mark (U+FEFF) at the start of the file. This invisible character becomes part of the first key, causing it to silently fail to match. Save your
.envas UTF-8 without BOM. - Windows line endings (CRLF) — If your file uses
\r\nline endings, the\rmay become part of the value. Most modern parsers handle this, but older versions may not. Configure your editor to use LF line endings for.envfiles. - Trailing whitespace — Unquoted values may silently include trailing spaces. This is especially dangerous for API keys and connection strings. Double-quote values when precision matters.
- Inline comments on unquoted values — The line
KEY=value # commentsets the value tovaluein Node.js dotenv, but tovalue # commentin parsers that do not support inline comments. Always quote values if they might appear alongside a#. - Dollar signs in passwords — An unquoted value like
PASS=my$ecretwill trigger variable expansion in parsers that support it, replacing$ecretwith an empty string. Use single quotes:PASS='my$ecret'. - Duplicate keys — When a key appears more than once, most parsers use the last occurrence. However, this behavior is not guaranteed. Avoid duplicate keys entirely.
What does a complete .env file look like?
This example demonstrates every syntax feature described above in a single file.
# Basic key-value pairs
NODE_ENV=development
PORT=3000
# Double-quoted value with spaces and escape sequences
GREETING="Hello, World!"
MULTILINE="first line\nsecond line"
# Single-quoted literal value — no interpolation
REGEX='\d{3}-\d{4}'
# Variable expansion (requires dotenv-expand in Node.js)
BASE_URL=https://api.example.com
FULL_URL=${BASE_URL}/v2/users
# Empty values
OPTIONAL_FLAG=
ALSO_EMPTY=""
# Export prefix (stripped by most parsers, not Docker Compose)
export LOG_LEVEL=debug
# Multiline value with real newlines
PRIVATE_KEY="-----BEGIN EC KEY-----
MHQCAQEEIBkg...
-----END EC KEY-----"References
- motdotla/dotenv — parsing rules — the canonical Node.js dotenv parser and its rule list.
- python-dotenv — file format — Python's reference dotenv implementation, covering quoting and multiline support.
- godotenv — examples — Go port of the Ruby dotenv with comments-and-exports support.
- Docker Compose — .env file syntax — official reference for the Compose-specific parser, including the `:` delimiter and inline-comment rules.
- dotenvy — dotenv file format — Elixir's deliberate divergence from the original spec, useful as a contrast.
Related on env.dev
- The complete .env guide — setup, best practices, and language examples.
- Why is my .env file not loading? — most failures trace back to the syntax rules above.
- Sharing .env files securely — once the syntax is right, share the file without leaking secrets.
- Validate your .env file online — checks values against these parsing rules instantly.