dotenv Errors — Quotes, BOM, and Duplicate Keys

webdev dotenv env devops tools

Works on the laptop. The container exits because DATABASE_URL is undefined — or defined as "postgres://..." with the quotes baked into the string. Most dotenv incidents are not exotic parser bugs. They are quotes, UTF-8 BOM, duplicate keys, and line endings that disagree between Windows editors and Linux containers.

Mental model: a dialect, not bash

A .env file is not a shell script. It is a small KEY=value dialect interpreted by whichever loader you happen to use: Node’s dotenv, Docker Compose env_file, Vercel, Kubernetes secrets mounted as files, or a custom parser in another language. Dialects disagree on quotes, multiline, export prefixes, and whether empty values are allowed.

When two environments disagree, print the parsed value in a safe debug build — length and a redacted prefix — instead of re-reading the file with your eyes. Humans see PASSWORD="secret". The process may see "secret" including the quote characters.

NAME=value
# comment
EMPTY=

Quotes: when they help and when they poison values

# Often wrong if quotes are stored literally
PASSWORD="s3cret"

# Space without quotes — may truncate at space or error
DISPLAY_NAME=Ada Lovelace

# Safer when spaces are required (verify your loader)
DISPLAY_NAME="Ada Lovelace"

Rules of thumb that survive most teams:

  • Prefer no quotes for simple tokens (URLs without spaces, UUIDs, ports).
  • Use quotes when the value contains spaces or #.
  • Never assume export FOO=bar works in every loader.
  • After changing a quoted value, log JSON.stringify(process.env.FOO) once in staging so you see whether quotes survived.

UTF-8 BOM: the invisible first character

Windows editors sometimes save with a byte-order mark. The first key becomes \uFEFFDATABASE_URL. Lookup for DATABASE_URL fails even though the line “looks right.” Re-save as UTF-8 without BOM, or run the file through a formatter that strips leading BOM. If only the first variable is mysteriously missing while others load, BOM is the first suspect.

Duplicate keys: last wins, humans read first

API_URL=https://staging.example.com
API_URL=https://prod.example.com

Most loaders keep the last occurrence and stay silent. Reviewers skim the first. Format and sort keys so duplicates sit adjacent and fail code review loudly. Treat duplicate keys as a merge conflict that happened inside one file.

CRLF and Docker: trailing \r on hostnames

CRLF can leave \r at the end of values. Hostnames become db\r. TLS and DNS then fail in ways that look like “networking.” Normalize to LF in .gitattributes for env files and in editor settings.

*.env text eol=lf
*.env.* text eol=lf

Also confirm Compose is reading the file you think it is: path relative to the compose file, env_file order, and environment: overrides that win after the file loads.

Multiline and JSON values

Some loaders support double-quoted multiline; many production platforms do not. For JSON credentials, prefer a single line or a secret store. If you must keep JSON in env:

GOOGLE_CREDENTIALS_JSON={"type":"service_account",...}

Validate JSON immediately after load. Do not discover broken escaping at the first production deploy.

Checklist before you blame Kubernetes

  1. Is the file present at the path the process expects?
  2. Exact key spelling and case?
  3. BOM on the first line?
  4. Trailing \r from CRLF?
  5. Quotes included in the parsed value?
  6. A duplicate key later in the file?
  7. Shell-exported variables overriding the file?
  8. Compose env_file order or environment: overrides?

Run the file through an env formatter to normalize ordering, spacing, and obvious syntax noise before comparing environments. Keep secrets local — formatting does not mean uploading production .env to a stranger’s server; prefer offline or browser tools.

Habits that prevent the same page next quarter

Ship .env.example with every required key documented. Fail fast on missing required vars at process boot. Use a schema validator (zod, envalid, or similar) so types and presence are explicit. Prefer a secret manager for production; keep .env for local development.

Most “dotenv is broken” incidents are file encoding and human editing. Treat .env like code: consistent format, LF endings, no BOM, unique keys, and boot-time validation. Then Docker stops being mysterious and “works on my machine” becomes a checklist instead of folklore.