Environment variables keep secrets out of source code and let you change configuration without redeploying. The .env file has become the standard way to manage them locally. This guide covers syntax rules, framework setup, security, environment-specific files, Docker integration, common errors, and production alternatives.
.env Syntax Rules
A .env file is a plain-text file with one variable per line. Here are the rules every developer should know:
Basic Syntax
Each line follows the KEY=VALUE pattern. No spaces around the equals sign.
# .env
DATABASE_URL=postgres://localhost:5432/mydb
API_KEY=sk-1234567890abcdef
PORT=3000
DEBUG=trueQuoting Rules
Values can be unquoted, single-quoted, or double-quoted. The behavior differs:
# Unquoted — trailing whitespace trimmed
APP_NAME=My Application
# Single quotes — literal, no interpolation
PASSWORD='p@$$w0rd#123'
# Double quotes — escape sequences + interpolation
GREETING="Hello\nWorld"
FULL_URL="${BASE_URL}/api/v1"- Unquoted: trailing whitespace is trimmed, no escape sequences.
- Single-quoted: value is taken literally, no interpolation, no escapes.
- Double-quoted: supports escape sequences (\n, \t) and variable interpolation.
Multiline Values
Use double quotes and \n for newlines, or use actual newlines inside double quotes:
# Method 1: \n in double quotes
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIB...\n-----END RSA PRIVATE KEY-----"
# Method 2: actual newlines in double quotes
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"Comments
Lines starting with # are comments. Inline comments work only with unquoted values in some parsers:
# This is a full-line comment
DATABASE_URL=postgres://localhost/mydb # inline comment (some parsers)
# Empty lines are ignored
API_KEY=abc123 # This may or may not work depending on the parserVariable Interpolation
Reference other variables using ${VAR} syntax (supported by most parsers in double-quoted values):
BASE_URL=https://api.example.com
API_V1=${BASE_URL}/v1
API_V2=${BASE_URL}/v2
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DATABASE_URL=postgres://${DB_HOST}:${DB_PORT}/${DB_NAME}Framework Setup
Node.js (dotenv)
The most popular .env loader for Node.js. Install and configure:
# Install
npm install dotenv
# --- app.js (CommonJS) ---
require('dotenv').config();
console.log(process.env.DATABASE_URL);
# --- app.ts (ES Modules) ---
import 'dotenv/config';
console.log(process.env.DATABASE_URL);
# --- Or load from CLI ---
node -r dotenv/config app.js
node -r dotenv/config app.js dotenv_config_path=.env.localNext.js, Vite, and Create React App load .env files automatically -- no package needed.
Python (python-dotenv)
Load .env in Python projects:
# Install
pip install python-dotenv
# --- app.py ---
from dotenv import load_dotenv
import os
load_dotenv() # loads .env from current directory
# load_dotenv('.env.local') # or specify a path
database_url = os.getenv('DATABASE_URL')
print(database_url)
# --- Django: manage.py or settings.py ---
from dotenv import load_dotenv
from pathlib import Path
env_path = Path('.') / '.env'
load_dotenv(dotenv_path=env_path)Go (godotenv)
Load .env in Go projects:
// Install
// go get github.com/joho/godotenv
package main
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load() // loads .env
if err != nil {
log.Fatal("Error loading .env file")
}
dbURL := os.Getenv("DATABASE_URL")
fmt.Println(dbURL)
}PHP (vlucas/phpdotenv)
Used by Laravel and most PHP frameworks:
// Install
// composer require vlucas/phpdotenv
<?php
require 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Access variables
$dbUrl = $_ENV['DATABASE_URL'];
// or
$dbUrl = getenv('DATABASE_URL');
// Required variables (throws exception if missing)
$dotenv->required(['DATABASE_URL', 'API_KEY']);Ruby (dotenv gem)
Used by Rails and other Ruby projects:
# Gemfile
gem 'dotenv-rails', groups: [:development, :test]
# Or for non-Rails Ruby:
gem 'dotenv'
# --- app.rb ---
require 'dotenv/load'
puts ENV['DATABASE_URL']
# --- Rails: .env is loaded automatically ---
# Access via ENV['DATABASE_URL'] anywhere in your appSecurity: Never Commit .env Files
Your .env file contains secrets (API keys, database passwords, tokens). Committing it to version control is the #1 security mistake.
.gitignore Patterns
Add these patterns to your .gitignore file immediately:
# .gitignore
# Ignore all .env files
.env
.env.local
.env.*.local
.env.development
.env.production
# More aggressive pattern — ignore all .env variants
.env*
# But DO commit the example file
!.env.exampleUse .env.example
Commit a .env.example file with empty values so teammates know which variables are required:
# .env.example — commit this file
# Copy to .env and fill in your values
DATABASE_URL=
API_KEY=
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
REDIS_URL=
JWT_SECRET=
# Optional
DEBUG=false
LOG_LEVEL=infoAlready Committed .env?
If you accidentally committed a .env file, remove it from tracking and rotate all secrets:
# Step 1: Remove .env from Git tracking (keeps local file)
git rm --cached .env
# Step 2: Add to .gitignore
echo ".env" >> .gitignore
# Step 3: Commit the removal
git add .gitignore
git commit -m "Remove .env from tracking, add to .gitignore"
# Step 4: CRITICAL — Rotate ALL secrets in the .env file
# Every API key, password, and token that was exposed
# must be regenerated immediatelyEnvironment-Specific Files
Most frameworks support multiple .env files for different environments. Understanding the loading order is critical.
Loading Order (Next.js / Vite / CRA)
Files are loaded in this priority (later files override earlier ones):
# Loading priority (highest to lowest):
# 1. Shell environment variables (always win)
# 2. .env.{NODE_ENV}.local (e.g. .env.production.local)
# 3. .env.local (NOT loaded in test)
# 4. .env.{NODE_ENV} (e.g. .env.production)
# 5. .env (default fallback)
# Example for NODE_ENV=production:
# .env → loaded first (base defaults)
# .env.production → overrides .env
# .env.local → overrides .env.production
# .env.production.local → overrides everything above.env.local is always ignored by git (add it to .gitignore). It is NOT loaded during testing to keep tests deterministic.
Which File for What?
| File | Purpose | Git |
|---|---|---|
| .env | Default values, non-secret config | Commit |
| .env.example | Template with empty values | Commit |
| .env.local | Local secrets & overrides | Ignore |
| .env.development | Dev-specific (shared) | Commit |
| .env.production | Production-specific (non-secret) | Commit |
| .env.test | Test environment config | Commit |
| .env.production.local | Production secrets (local only) | Ignore |
Docker & Docker Compose Integration
env_file Directive
Docker Compose can load .env files directly into containers:
# docker-compose.yml (or compose.yml)
services:
web:
image: node:20-alpine
env_file:
- .env # base defaults
- .env.production # production overrides
ports:
- "3000:3000"
db:
image: postgres:16
env_file:
- .env.db # separate file for DB secrets
volumes:
- pgdata:/var/lib/postgresql/dataenvironment vs env_file
You can also inline environment variables. Here is the difference:
# Using env_file (loads from file)
services:
web:
env_file:
- .env
# Using environment (inline)
services:
web:
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=${DATABASE_URL} # from shell or .env
# Using environment (mapping syntax)
services:
web:
environment:
NODE_ENV: production
PORT: 3000- env_file: loads from file, keeps compose.yml clean, easy to swap per environment.
- environment: visible in compose.yml, good for non-secret values, supports variable substitution.
.env for Docker Compose Variables
Docker Compose automatically reads a .env file in the project root for variable substitution in compose.yml:
# .env (in same directory as compose.yml)
POSTGRES_VERSION=16
NODE_VERSION=20
APP_PORT=3000
# compose.yml — uses .env for variable substitution
services:
web:
image: node:${NODE_VERSION}-alpine
ports:
- "${APP_PORT}:3000"
db:
image: postgres:${POSTGRES_VERSION}
environment:
POSTGRES_DB: myapp10 Common .env Errors and Fixes
Here are the most frequent .env problems developers encounter:
| # | Error | Cause | Fix |
|---|---|---|---|
| 1 | process.env.VAR is undefined | dotenv not loaded or loaded after usage | Call dotenv.config() at the very top of your entry file |
| 2 | Variables empty in production | .env file not deployed; relying on file instead of platform env vars | Set env vars in your hosting platform (Vercel, AWS, etc.) |
| 3 | .env values have quotes in them | Parser treats quotes as literal characters | Check your parser docs; most strip double quotes but not all |
| 4 | Wrong .env file loaded | Working directory differs from expected path | Use path option: dotenv.config({ path: ".env.local" }) |
| 5 | Multiline value truncated | Value not properly double-quoted | Wrap multiline values in double quotes |
| 6 | Special characters break value | $ or # interpreted as interpolation/comment | Use single quotes to prevent interpolation, or escape with \ |
| 7 | BOM encoding error | .env saved with UTF-8 BOM by Windows editors | Save as UTF-8 without BOM; first variable may be unreadable otherwise |
| 8 | Docker container env vars empty | env_file path incorrect or file not in build context | Verify path is relative to compose.yml location |
| 9 | Spaces around = break parsing | KEY = VALUE instead of KEY=VALUE | Remove spaces around the equals sign |
| 10 | Variables not available in browser (React/Next) | Missing required prefix (NEXT_PUBLIC_ or REACT_APP_) | Add the framework-required prefix to expose to client-side code |
Production Alternatives
In production, .env files are not recommended. Use these alternatives instead:
Platform Environment Variables
Every major hosting platform provides a UI or CLI for setting environment variables:
# Vercel
vercel env add DATABASE_URL production
vercel env ls
# AWS (Parameter Store)
aws ssm put-parameter \
--name "/myapp/prod/DATABASE_URL" \
--value "postgres://..." \
--type SecureString
# Heroku
heroku config:set DATABASE_URL=postgres://...
heroku config
# Railway
railway variables set DATABASE_URL=postgres://...
# Fly.io
fly secrets set DATABASE_URL=postgres://...Docker Secrets
For Docker Swarm or Compose, use secrets for sensitive data:
# compose.yml with Docker secrets
services:
web:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt # for Compose
# external: true # for Swarm
api_key:
file: ./secrets/api_key.txt
# In your app, read from file:
# const secret = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();Secret Managers (Vault, AWS Secrets Manager)
For enterprise applications, use a dedicated secret manager:
# HashiCorp Vault
vault kv put secret/myapp/production \
DATABASE_URL="postgres://..." \
API_KEY="sk-..."
# Read in application
vault kv get -field=DATABASE_URL secret/myapp/production
# AWS Secrets Manager
aws secretsmanager create-secret \
--name "myapp/production/db" \
--secret-string '{"url":"postgres://...","password":"..."}'
# Read in Node.js with AWS SDK
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManager({ region: 'us-east-1' });
const { SecretString } = await client.getSecretValue({
SecretId: 'myapp/production/db'
});
const secrets = JSON.parse(SecretString);Secret managers provide rotation, audit logging, access control, and encryption at rest -- features .env files cannot offer.
Frequently Asked Questions
Should I commit my .env file to Git?
Never commit .env files that contain secrets (API keys, passwords, tokens) to version control. Instead, commit a .env.example file with empty placeholder values so teammates know which variables are needed. Add .env* patterns to your .gitignore file.
What is the difference between .env, .env.local, and .env.production?
.env contains default values loaded in all environments. .env.local contains local overrides and is not committed to git. .env.production contains production-specific values loaded only when NODE_ENV=production. The loading order is: .env, then .env.local, then .env.[environment], then .env.[environment].local, with later files overriding earlier ones.
Why is process.env.MY_VAR undefined in Node.js?
The most common causes are: 1) You forgot to install and call require("dotenv").config() at the top of your entry file, 2) Your .env file is not in the root directory where Node runs, 3) The variable name has a typo, 4) You are using ES modules and need to use "import dotenv/config" instead.
How do I use .env variables in Docker Compose?
Docker Compose automatically reads a .env file in the same directory as compose.yml for variable substitution using ${VAR} syntax. For passing variables into containers, use the env_file directive to load from a file, or the environment key to set them inline in compose.yml.
Can I use .env files in production?
It is not recommended. In production, use platform-provided environment variables (Vercel, AWS, Heroku dashboards), Docker secrets, or a secret manager like HashiCorp Vault or AWS Secrets Manager. These provide better security with encryption, rotation, audit logging, and access control.
How do I expose .env variables to the browser in React or Next.js?
In Next.js, prefix variables with NEXT_PUBLIC_ (e.g., NEXT_PUBLIC_API_URL). In Create React App, use REACT_APP_ prefix. In Vite, use VITE_ prefix. Only prefixed variables are embedded in the client bundle. Never expose secrets this way -- only use it for public configuration like API endpoints.