env.dev

dotenv Not Loading? Step-by-Step Debugging Guide

Fix environment variables not loading from .env files. Covers Node.js, Python, Docker, file path issues, syntax errors, load order, and production gotchas.

Last updated:

When process.env.MY_VAR returns undefined, the cause is almost always one of nine things: the .env file is missing, dotenv is not loaded early enough, the file path is wrong, import order defeats configuration, production has no .env file, there is a syntax error in the file, Windows line endings corrupt values, an existing environment variable takes precedence, or a framework-specific convention was missed. This guide walks through each cause with a concrete fix. Use our env validator to catch syntax issues instantly, and see the full .env guide for syntax reference.

1. Does the .env file actually exist?

The most common cause is trivial: the file is not where you think it is. Dotenv libraries default to reading .env from the current working directory (process.cwd() in Node.js, os.getcwd() in Python), which is wherever you ran the command — not necessarily the project root.

bash
# Verify the file exists from your project root
ls -la .env

# Print the working directory your app actually sees
node -e "console.log(process.cwd())"
python -c "import os; print(os.getcwd())"

If you run your app from a subdirectory (common in monorepos), the file will not be found. Either move the file or pass an explicit path.

2. Is dotenv installed and imported early enough?

Dotenv does not ship with Node.js or Python — you must install and invoke it yourself. If the import is missing or happens after other modules read process.env, those modules will see undefined.

bash
# Node.js
npm install dotenv

# Python
pip install python-dotenv

In Node.js, import dotenv at the very top of your entry file, before any other imports:

typescript
// index.ts — this MUST be the first import
import 'dotenv/config';

// Now other modules can safely read process.env
import { app } from './app.ts';

In Python, call load_dotenv() before accessing os.environ:

python
from dotenv import load_dotenv
load_dotenv()  # Must come before os.getenv() calls

import os
db_url = os.getenv("DATABASE_URL")

3. Is the file path wrong?

Relative paths resolve against the working directory, not the file that calls dotenv.config(). In monorepos or when running from a different directory, use an absolute path:

typescript
import dotenv from 'dotenv';
import path from 'node:path';

// Resolve relative to this file, not cwd
dotenv.config({ path: path.resolve(__dirname, '../.env') });

// Or in ESM (no __dirname)
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, '../.env') });

In Python, the same principle applies:

python
from pathlib import Path
from dotenv import load_dotenv

env_path = Path(__file__).resolve().parent.parent / '.env'
load_dotenv(dotenv_path=env_path)

4. Are modules importing before dotenv.config() runs?

ES module imports are hoisted. If you call dotenv.config() in the module body but import other modules in the same file, those imports execute first. The fix is to use the side-effect import style:

typescript
// WRONG — config() runs after db.ts is already imported
import dotenv from 'dotenv';
import { db } from './db.ts';  // db.ts reads process.env.DATABASE_URL → undefined
dotenv.config();

// CORRECT — side-effect import runs immediately
import 'dotenv/config';        // Runs first due to side-effect execution
import { db } from './db.ts';  // process.env.DATABASE_URL is now set

Alternatively, use the --require or --import flag to preload dotenv before any application code:

bash
# CommonJS
node --require dotenv/config src/index.js

# ESM (Node 20.6+)
node --import dotenv/config src/index.ts

5. Why are variables undefined in production?

Production environments do not use .env files. The file should not be deployed and should be in .gitignore. In production, set real environment variables through your hosting platform:

bash
# Heroku
heroku config:set DATABASE_URL=postgres://...

# Docker
docker run -e DATABASE_URL=postgres://... myapp

# Cloudflare Workers
wrangler secret put DATABASE_URL

# AWS ECS / Lambda — use Parameter Store or Secrets Manager
# Kubernetes — use ConfigMaps or Secrets

If you conditionally load dotenv only in development, make sure production has every variable your app expects. Validate at startup to catch missing variables before they cause runtime errors.

6. Are there syntax errors in the .env file?

The .env format is deceptively simple. These common mistakes silently break parsing:

Spaces around the equals sign

ini
# WRONG — most parsers treat " value" as the value (with leading space) or error
DATABASE_HOST = localhost

# CORRECT
DATABASE_HOST=localhost

Missing quotes around values with special characters

ini
# WRONG — the # starts an inline comment, truncating the value
CALLBACK_URL=https://example.com/auth#callback

# CORRECT
CALLBACK_URL="https://example.com/auth#callback"

UTF-8 BOM character

Some editors (especially on Windows) prepend an invisible BOM (U+FEFF) to the file. This corrupts the first variable name, making it unrecognizable.

bash
# Detect BOM
file .env
# Output containing "BOM" means the file has one

# Remove BOM
sed -i '1s/^\xEF\xBB\xBF//' .env

Run your file through the env validator to catch these issues automatically.

7. Are Windows line endings (CRLF) corrupting values?

If your .env file uses Windows line endings (\r\n), the trailing \r may be included in the value. This causes subtle bugs: process.env.DB_HOST would be "localhost\r" instead of "localhost", and connection strings silently fail.

bash
# Detect CRLF
cat -A .env | head -5
# Lines ending in ^M$ have CRLF

# Convert to LF
sed -i 's/\r$//' .env

# Or use dos2unix
dos2unix .env

Prevent this permanently by adding a .gitattributes rule:

ini
# .gitattributes
.env* text eol=lf

8. Is an existing environment variable taking precedence?

By default, dotenv does not overwrite variables that already exist in the environment. If DATABASE_URL is set in your shell, your .env file's DATABASE_URL will be ignored.

bash
# Check if a variable is already set in your shell
echo $DATABASE_URL
printenv | grep DATABASE_URL

In Node.js, use the override option if you want the file to win:

typescript
import dotenv from 'dotenv';

// Force .env values to override existing env vars
dotenv.config({ override: true });

In Python, load_dotenv behaves the same way:

python
from dotenv import load_dotenv

# Override existing environment variables
load_dotenv(override=True)

9. What framework-specific gotchas should I watch for?

Node.js with dotenv-expand

If you use variable interpolation (${BASE_URL}/api), you need dotenv-expand in addition to dotenv:

typescript
import dotenv from 'dotenv';
import { expand } from 'dotenv-expand';

expand(dotenv.config());
// Now ${BASE_URL}/api resolves correctly

Python — load_dotenv path resolution

By default, load_dotenv() searches the current directory and walks up parent directories. If your project structure is deep, it may find the wrong file or none at all. Always pass an explicit path in complex projects:

python
from dotenv import load_dotenv, find_dotenv

# find_dotenv() walks up directories to locate .env
load_dotenv(find_dotenv())

# Or be explicit
load_dotenv('/app/.env')

For deeper Python coverage (Django, Flask, FastAPI, virtualenv interactions), see the Python environment variables guide.

Docker — build time vs runtime

Environment variables set with ENV in a Dockerfile are baked into the image and available at runtime. Variables set with ARG are only available during build and are not present at runtime. A common mistake is using ARG for runtime config:

dockerfile
# WRONG — ARG is not available at runtime
ARG DATABASE_URL
RUN echo $DATABASE_URL  # Works during build only

# CORRECT — use ENV for runtime, ARG for build-only values
ARG BUILD_HASH
ENV DATABASE_URL=postgres://db:5432/app
RUN echo "Build: $BUILD_HASH"

In multi-stage builds, ARG and ENV values do not carry over between stages. You must redeclare them in each stage that needs them:

dockerfile
FROM node:20 AS builder
ARG NODE_ENV=production
RUN npm run build

FROM node:20-slim
# Must redeclare — the builder's ARG is not inherited
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist

Quick Debugging Checklist

  1. Confirm the .env file exists at the expected path
  2. Confirm dotenv is installed and imported before any code that reads env vars
  3. Use import 'dotenv/config' instead of calling dotenv.config() after other imports
  4. Check for whitespace around =, missing quotes, and BOM characters
  5. Run cat -A .env to reveal hidden \r characters
  6. Run printenv | grep VAR_NAME to check for collisions with existing env vars
  7. In production, set variables through your platform — never deploy a .env file

References

Related guides

Was this helpful?

Read next

Node.js Env Variables: process.env, dotenv & --env-file

How to use environment variables in Node.js: process.env, dotenv, the Node 20.6+ --env-file flag, NODE_ENV, type-safe validation with zod.

Continue →

Frequently Asked Questions

Why is my .env file not being loaded?

The most common causes are: the dotenv package is not installed or not imported early enough, the .env file is not in the working directory, there are syntax errors in the file, or you are in production where .env files are not used.

Does dotenv work in production?

Most dotenv libraries load .env files but do not override existing environment variables. In production, set real environment variables through your hosting platform, CI/CD pipeline, or container orchestration. The .env file is for local development only.

Why are my environment variables undefined after dotenv.config()?

Check that dotenv.config() is called before any module that reads process.env. In Node.js, imports are hoisted, so use the side-effect import "dotenv/config" at the very top of your entry file so dotenv runs before any other module reads process.env.

Why does my .env file work locally but not in CI/CD?

CI/CD runners do not source .env files automatically — they are usually gitignored, so they never reach the runner in the first place. Set the variables in your pipeline config (GitHub Actions secrets, GitLab CI variables, CircleCI contexts) or inject them as real environment variables before your build/test steps run.

Does Node 20.6+ replace dotenv?

Partially. Node 20.6+ ships a built-in --env-file=.env flag (stable since v24.10) that loads variables without any package. It covers the basic case but lacks variable expansion, multiple-file merging beyond simple override, a programmatic API, and TypeScript types. For most apps the dotenv package is still the more flexible choice; for short-lived scripts the built-in flag is enough.

Stay up to date

Get notified about new guides, tools, and cheatsheets.