How to use dotenv — one guide, every stack
Every modern framework has a blessed way to load .env files. The patterns are 90% identical — you install a library, call a load function, then read from process env. Here's the quickstart for each stack: Node.js, React, Next.js, webpack, Django, Rails, Laravel, Go, and Docker.
Node.js dotenv — every way to load it
Three options, in order of how most Node.js projects actually use dotenv:
1. dotenv/config (zero-code import)
The fastest way to wire up dotenv — just import the config sub-module and it loads .env as a side effect, before anything else runs:
npm install dotenv
// ESM — must be the first import
import 'dotenv/config';
// CommonJS — run before requiring anything that reads process.env
require('dotenv/config');2. require('dotenv').config() (explicit call)
Same result, but gives you a return value (parsed keys, or an error) and lets you pass options like a custom path:
const dotenv = require('dotenv');
const result = dotenv.config({ path: '.env.local' });
if (result.error) throw result.error;
console.log(process.env.DATABASE_URL);3. Node's native --env-file flag (Node 20+, no package)
node --env-file=.env server.js
# or multiple files (later overrides earlier)
node --env-file=.env --env-file=.env.local server.js4. dotenv-cli — for scripts that don't import dotenv
Useful for one-off commands (migrations, seed scripts, other CLIs) that need .env values without any code changes:
npm install -D dotenv-cli
# package.json
"scripts": { "migrate": "dotenv -e .env.local -- prisma migrate dev" }React (Create React App and Vite)
Plain React has no built-in .env loader — it's the build tool that decides. Neither CRA nor Vite use the dotenv package directly; both inline prefixed variables into the bundle at build time.
Create React App
# .env — must start with REACT_APP_
REACT_APP_API_URL=https://api.example.com
// src/App.jsx
console.log(process.env.REACT_APP_API_URL);Vite
# .env — must start with VITE_
VITE_API_URL=https://api.example.com
// src/main.ts
console.log(import.meta.env.VITE_API_URL);See NEXT_PUBLIC_ vs VITE_ prefix rules if you're porting a Vite app to Next.js or vice versa.
webpack (without a framework)
Raw webpack setups need a plugin — webpack doesn't read .env on its own. The two common options:
npm install dotenv-webpack --save-dev
// webpack.config.js
const Dotenv = require('dotenv-webpack');
module.exports = {
plugins: [new Dotenv()],
};Alternatively, load with dotenv in webpack.config.js itself and pass values through DefinePlugin — more boilerplate, but no extra dependency and full control over which keys get exposed to the bundle.
Next.js (App Router)
Next.js reads .env files automatically. No import needed. The load order is:
.env— committed defaults.env.local— your machine, always loaded (never in tests).env.developmentor.env.production— env-specific
Variables are server-only by default. To expose to the browser, prefix with NEXT_PUBLIC_:
# .env.local
DATABASE_URL=postgres://localhost/app # server only
NEXT_PUBLIC_API_URL=https://api.example.com # browser + serverRead them like anywhere else: process.env.DATABASE_URL, process.env.NEXT_PUBLIC_API_URL.
Python / Django
pip install python-dotenv
# settings.py
from pathlib import Path
from dotenv import load_dotenv
import os
load_dotenv(Path(__file__).resolve().parent.parent / '.env')
SECRET_KEY = os.environ['SECRET_KEY']
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DATABASE_NAME'],
},
}Ruby / Rails — dotenv-rails
Rails doesn't load .env on its own; the dotenv-rails gem hooks into the boot process:
# Gemfile
group :development, :test do
gem 'dotenv-rails'
end
$ bundle install
# .env (auto-loaded at boot, before config/application.rb runs)
DATABASE_URL=postgres://localhost/app
# Access anywhere
ENV['DATABASE_URL']Plain Ruby (no Rails) uses the same gem without the -rails suffix — gem 'dotenv' plus Dotenv.load at the top of your script.
Laravel
Laravel ships with vlucas/phpdotenv. The .env file is read once at boot. Access only from config files:
# .env
APP_KEY=base64:...
DB_CONNECTION=mysql
DB_DATABASE=laravel
// config/database.php
'database' => env('DB_DATABASE', 'forge'),Never call env() outside config files in production — config:cache freezes values at build time.
Go
// go.mod
require github.com/joho/godotenv v1.5.1
// main.go
import "github.com/joho/godotenv"
func main() {
godotenv.Load()
dbURL := os.Getenv("DATABASE_URL")
}Docker / docker-compose
# docker-compose.yml
services:
api:
env_file:
- .env
# or inline:
environment:
- NODE_ENV=productionNeed to ship the whole .env to Kubernetes? Convert it to a K8s Secret manifest in one click.
Load-order rules (across stacks)
- Later files override earlier files.
- Shell environment beats
.envfiles (almost always). .env.localis intentionally ignored by tests in most frameworks.- Production builds usually don't read
.envat runtime — the host injects vars.
When in doubt, use the .env merger to preview what your final config will look like.
FAQ
What does require('dotenv').config() actually do?
It reads .env from your project root (or a path you pass in), parses each KEY=VALUE line, and writes every key into process.env — but only if that key isn't already set. Existing process.env values always win, which is why shell exports and CI secrets override .env without any extra config.
What's the difference between dotenv, dotenv/config, and dotenv-cli?
dotenv is the library you call with require('dotenv').config(). dotenv/config is a zero-code shortcut for the same thing — import it and it runs on import. dotenv-cli is a separate npm package (dotenv-cli) that loads .env before running any shell command, useful for scripts that don't import dotenv themselves.
Does dotenv work in the browser?
No — dotenv reads from the filesystem, which only exists server-side (Node.js). For browser code, bundlers inline specific env vars into the JavaScript at build time instead: Next.js uses the NEXT_PUBLIC_ prefix, Vite uses VITE_, and Create React App uses REACT_APP_.
Why isn't my .env variable showing up?
The most common causes: the load call runs after the code that reads process.env, the key isn't prefixed correctly for a browser bundler (NEXT_PUBLIC_/VITE_/REACT_APP_), or a shell-exported variable with the same name is silently overriding the .env value. Walk through the full checklist in how to fix .env file errors.
For the deeper mechanics of how dotenv and process.env relate, read dotenv vs process.env in Node.js.
Next steps
If a variable doesn't load, walk through how to fix .env file errors. For team-wide hygiene, .env best practices is the next read.