Fix: .env File Not Loading – dotenv Variables Are Undefined in Node.js, Python, or Docker
Quick Answer
How to fix .env files not being loaded by dotenv, Next.js, Vite, Django, or Docker Compose, including wrong file path, missing dotenv.config(), and variable naming issues.
The Error
You have a .env file with your variables defined, but your application acts like it does not exist. The variables come back as undefined (Node.js), None (Python), or empty strings (Docker):
// Node.js
console.log(process.env.DATABASE_URL);
// undefined# Python
import os
print(os.getenv("DATABASE_URL"))
# None# docker-compose.yml -- variables from .env are empty
services:
app:
environment:
- DATABASE_URL=${DATABASE_URL}The .env file is right there in your project. You can see it. But something between the file and your runtime is broken, and the variables never get loaded.
This is different from a variable being undefined because of a missing prefix or typo. Here, the entire .env file is being ignored. None of the variables load, or the file itself is not found by the loader.
Why This Happens
A .env file is just a text file. It does nothing on its own. Something in your toolchain must explicitly read it, parse each line, and inject the key-value pairs into the process environment. When that step fails or never runs, every variable from the file is missing.
The most common reasons the file is not loaded:
dotenv.config()or its equivalent was never called. Node.js, Python, and most languages do not read.envfiles by default. You need a library or a framework feature to do it.- The file is named wrong. The file must be called exactly
.env(or a recognized variant like.env.local). A file namedenv,.env.txt, or.ENVwill not be picked up. - The file is in the wrong directory. Most tools look for
.envrelative to the working directory or the project root. If your script runs from a subdirectory, the file will not be found. - The framework requires a specific prefix on variable names, and without it, variables are silently excluded from the client-side bundle.
- Docker Compose is not configured to use the file. The
env_filedirective is missing, or the file path is wrong. - Variable expansion is breaking values. Characters like
$in values trigger variable interpolation, corrupting passwords and connection strings. - The file exists locally but is missing from the deployed environment. Because
.envis in.gitignore, it never reaches the server, container, or CI pipeline.
Fix 1: Call dotenv.config() in Node.js
Plain Node.js and Express do not read .env files. You need the dotenv package, and you need to call it before anything else tries to read process.env.
Install it:
npm install dotenvThen load it at the very top of your entry file:
// CommonJS
require('dotenv').config();
const express = require('express');
const app = express();
console.log(process.env.DATABASE_URL); // now loadedIf you use ES modules:
import 'dotenv/config';
import express from 'express';The critical detail is import order. If another module reads process.env at import time and that import comes before dotenv/config, the variable will be undefined in that module. Always make dotenv the very first import.
A common pattern that fails:
// ❌ Wrong -- db.js reads process.env.DATABASE_URL at import time
import { connectDB } from './db.js';
import 'dotenv/config';
connectDB(); // DATABASE_URL is undefined inside db.js// ✅ Correct -- dotenv loads first
import 'dotenv/config';
import { connectDB } from './db.js';
connectDB(); // DATABASE_URL is availableNode.js 20.6+ built-in .env support
If you are on Node.js 20.6 or later, you can skip the dotenv package entirely:
node --env-file=.env app.jsThis loads the .env file before any of your code runs. You can also chain multiple files:
node --env-file=.env --env-file=.env.local app.jsFix 2: Use the Correct File Name and Path
The file must be named exactly .env (lowercase, starting with a dot, no extension). These common mistakes will cause loading to fail silently:
| Wrong name | Why it fails |
|---|---|
env | Missing the leading dot |
.env.txt | Extra extension — your OS may have added this |
.ENV | Case matters on Linux/macOS (works on Windows by accident) |
.env | Trailing space in the filename |
Check what file actually exists:
# Linux/macOS
ls -la .env*
# Windows (PowerShell)
Get-ChildItem -Force .env*The file also must be in the right directory. By default, dotenv in Node.js looks for .env relative to process.cwd() — the directory from which you ran the node command, not the directory where your script file lives.
If your project structure looks like this:
project/
├── .env
├── package.json
└── src/
└── server.jsAnd you run node src/server.js from project/, then dotenv.config() will find .env because process.cwd() is project/. But if you cd src && node server.js, it will look for src/.env, which does not exist.
To be safe, specify the path explicitly:
require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') });Pro Tip: If you are running your app through a process manager like PM2, a systemd unit, or a Docker entrypoint script, the working directory might not be what you expect. Always use an absolute or explicitly resolved path for the
.envfile in production-like environments to avoid silent loading failures.
Fix 3: Add the Required Prefix for Next.js or Vite
Both Next.js and Vite automatically load .env files — you do not need to install dotenv. But they deliberately filter which variables are exposed to browser-side code.
Next.js: NEXT_PUBLIC_ prefix
Only variables starting with NEXT_PUBLIC_ are available in client components. Without the prefix, the variable is available in server-side code (API routes, Server Components) but will be undefined on the client:
# .env
NEXT_PUBLIC_API_URL=https://api.example.com
SECRET_KEY=super-secret// Client component
console.log(process.env.NEXT_PUBLIC_API_URL); // "https://api.example.com"
console.log(process.env.SECRET_KEY); // undefined (by design)If all your variables appear undefined even in server-side code, the .env file itself is not being found. Make sure it is in the project root (next to package.json), not inside src/ or app/. Also, check for hydration mismatches if values differ between server and client rendering.
Vite: VITE_ prefix
Vite uses import.meta.env instead of process.env. Only variables prefixed with VITE_ are included in the client bundle:
# .env
VITE_API_URL=https://api.example.com
DB_PASSWORD=secretconsole.log(import.meta.env.VITE_API_URL); // "https://api.example.com"
console.log(import.meta.env.DB_PASSWORD); // undefinedIf import.meta.env.VITE_API_URL is still undefined, check the file location. Vite looks for .env in the project root (where vite.config.js lives). You can override this with the envDir option in your Vite config:
// vite.config.js
export default defineConfig({
envDir: './config',
});If you are getting import resolution errors in Vite, make sure your config file itself is not broken, as that can prevent .env loading too.
After changing .env in either framework, restart the dev server. Environment variables are read at startup.
Fix 4: Load .env in Python with python-dotenv
Python does not read .env files natively. The python-dotenv package handles this:
pip install python-dotenvThen load it in your code:
from dotenv import load_dotenv
import os
load_dotenv() # loads .env from the current working directory
database_url = os.getenv("DATABASE_URL")
print(database_url) # now has the value from .envIf the file is in a different location, pass the path:
from pathlib import Path
from dotenv import load_dotenv
env_path = Path(__file__).resolve().parent.parent / '.env'
load_dotenv(dotenv_path=env_path)Django
Django does not load .env files by default. You have two main approaches:
Option 1: python-dotenv in manage.py and wsgi.py:
# manage.py
import dotenv
def main():
dotenv.load_dotenv()
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
# ...Option 2: django-environ package:
pip install django-environ# settings.py
import environ
env = environ.Env()
environ.Env.read_env() # reads .env from BASE_DIR by default
SECRET_KEY = env('SECRET_KEY')
DEBUG = env.bool('DEBUG', default=False)If your Python variables are still None, make sure the .env file is where the loader expects it. Django projects commonly have this structure:
myproject/
├── .env
├── manage.py
└── myproject/
├── settings.py
└── wsgi.pyThe .env file goes next to manage.py, not inside the inner myproject/ directory. If you are dealing with missing Python modules, resolve that first, because a broken import of dotenv itself will prevent loading.
Common Mistake: Calling
load_dotenv()inside a function that runs late in the application lifecycle. Ifsettings.pyor another module readsos.getenv()at import time (module-level code), the variables need to be loaded before that module is imported. In Django, this means loading.envinmanage.pyor at the very top ofsettings.py, not in a view or a signal handler.
Fix 5: Configure env_file in Docker Compose
Docker Compose does not automatically use your .env file for container environment variables. There are two separate mechanisms, and confusing them is a frequent source of errors.
Mechanism 1: Variable substitution in docker-compose.yml
Docker Compose reads a .env file in the same directory as docker-compose.yml to substitute variables inside the compose file itself:
# docker-compose.yml
services:
app:
image: myapp:${APP_VERSION}# .env (next to docker-compose.yml)
APP_VERSION=1.2.3This substitutes ${APP_VERSION} so the image becomes myapp:1.2.3. But the variable APP_VERSION is not passed into the container’s environment. It only affects the compose file parsing.
Mechanism 2: env_file for container environment
To inject variables from a file into the container, use env_file:
# docker-compose.yml
services:
app:
build: .
env_file:
- .envNow every variable in .env is available inside the container as an environment variable.
You can also combine multiple files:
env_file:
- .env
- .env.productionIf your compose setup is producing other errors, check the Docker Compose troubleshooting guide to rule out syntax or configuration issues.
Common Docker .env problems
The .env file is not copied into the image. If your Dockerfile has COPY . . but .dockerignore excludes .env, the file will not be in the image. For most setups, you should not copy .env into the image at all. Instead, pass variables at runtime with env_file or docker run --env-file .env.
Path is wrong in env_file. The path in env_file is relative to the compose file’s directory, not the project root or the build context:
# If docker-compose.yml is in deploy/ and .env is in the project root:
env_file:
- ../.envFix 6: Handle Variable Expansion and Special Characters
Most .env parsers support variable expansion using $ syntax. This can break values that contain $ literally, such as passwords:
# .env
PASSWORD=p@$$w0rdThe parser may try to expand $$w0rd as a reference to a variable named $w0rd, producing a corrupted value.
Fix: Wrap values containing $ in single quotes (if the parser supports it) or escape the dollar sign:
# Option 1: Single quotes (supported by most parsers)
PASSWORD='p@$$w0rd'
# Option 2: Escape the dollar sign
PASSWORD=p@\$\$w0rdIn dotenv for Node.js, variable expansion is off by default in the main dotenv package but enabled in dotenv-expand. If you are using dotenv-expand, be aware of this:
# .env
BASE_URL=https://api.example.com
API_URL=${BASE_URL}/v2require('dotenv').config();
require('dotenv-expand').expand(/* ... */);
console.log(process.env.API_URL); // "https://api.example.com/v2"If you do not want expansion, do not use dotenv-expand, or quote the value with single quotes.
Docker Compose also expands $ in .env files. To use a literal $, double it:
# .env for Docker Compose
PASSWORD=p@$$$$w0rd
# Results in: p@$$w0rdYes, this is confusing. The safest approach is to avoid $ in values entirely when possible, or use a secrets manager instead.
Fix 7: Ensure .env Exists in Deployed Environments
The .env file should be in .gitignore to avoid committing secrets. But this means it will not exist when you clone the repo on a server, in a CI pipeline, or inside a Docker build:
# .gitignore
.env
.env.local
.env.*.localThis is correct for security. But you need another mechanism to provide the variables in each environment:
- CI/CD pipelines (GitHub Actions, GitLab CI, etc.): Set variables in the pipeline’s secret management. In GitHub Actions, use repository or environment secrets and reference them with
${{ secrets.DATABASE_URL }}. - Docker/Kubernetes: Use
env_file, Kubernetes Secrets, or a secrets manager. - Hosting platforms (Vercel, Railway, Render, Fly.io): Set variables through the platform’s dashboard or CLI.
Create a .env.example file listing every required variable without values, and commit it:
# .env.example
DATABASE_URL=
API_KEY=
REDIS_URL=
NEXT_PUBLIC_API_URL=This serves as documentation for anyone deploying the project.
Still Not Working?
If you have tried everything above and your .env file still is not loading, work through this checklist:
Print the resolved path. Confirm dotenv is looking where you think:
// Node.js
const path = require('path');
const result = require('dotenv').config({
path: path.resolve(process.cwd(), '.env'),
debug: true,
});
console.log(result);
// If .error exists, it will tell you what went wrong# Python
from dotenv import load_dotenv, find_dotenv
print(find_dotenv()) # prints the path it found, or empty string if not foundCheck for BOM (byte order mark). Some Windows editors save files with a UTF-8 BOM. This invisible character at the start of the file can make the first variable name unreadable. Re-save the file as UTF-8 without BOM.
Check file permissions. On Linux, make sure the application user can read the file:
ls -la .env
# Should show at least -rw-r--r-- or -rw-------Check for override conflicts. By default, dotenv in Node.js does not overwrite variables that already exist in the shell environment. If DATABASE_URL is already set in your shell (perhaps from a profile script), the .env value is silently ignored:
require('dotenv').config({ override: true }); // forces .env to winIn Python:
load_dotenv(override=True)Check for multiline values. If a value spans multiple lines (like an RSA key), the syntax varies by parser. In dotenv for Node.js, wrap it in double quotes with literal newlines:
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"Verify the process is actually using your code. If you are running behind a process manager (PM2, supervisor) or in a container, make sure the running process is the version of code that includes the dotenv.config() call. An old cached build or a stale container image is a surprisingly common cause of “I added dotenv but nothing changed.”
Related: For broader environment variable issues including type coercion, prefix requirements, and framework-specific behavior, see Fix: process.env.VARIABLE_NAME Is Undefined.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Redis ECONNREFUSED – Connection Refused on localhost:6379
How to fix the Redis ECONNREFUSED error when your application cannot connect to the Redis server on localhost:6379 or a remote host. Covers Redis not running, wrong host/port, Docker networking, firewall, AUTH required, maxclients, and TLS configuration.
Fix: SSL certificate problem: unable to get local issuer certificate
How to fix 'SSL certificate problem: unable to get local issuer certificate', 'CERT_HAS_EXPIRED', 'ERR_CERT_AUTHORITY_INVALID', and 'self signed certificate in certificate chain' errors in Git, curl, Node.js, Python, Docker, and more. Covers CA certificates, corporate proxies, Let's Encrypt, certificate chains, and self-signed certs.
Fix: AWS Lambda Unable to import module / Runtime.ImportModuleError
How to fix the AWS Lambda Runtime.ImportModuleError and Unable to import module error caused by wrong handler paths, missing dependencies, layer issues, and packaging problems.
Fix: Docker container health status unhealthy
How to fix Docker container health check failing with unhealthy status, including HEALTHCHECK syntax, timing issues, missing curl/wget, endpoint problems, and Compose healthcheck configuration.