Dotenv — Complete Implementation Guide
In this tutorial, you will learn about Dotenv. We cover key concepts, practical examples, and best practices to help you master this topic.
Dotenv loads environment variables from a .env file into Process.env, simplifying local development by providing a convenient way to set configuration without modifying system environment variables.
What You'll Learn
By the end of this tutorial, you will know how to use dotenv for local development, structure .env files, manage multiple environments, and avoid common security pitfalls.
Why It Matters
Dotenv is the standard tool for managing local development configuration. It keeps environment-specific settings out of code while providing a simple, reproducible setup for every developer.
Real-World Use
DodaTech's development onboarding consists of cloning the repo and running cp .env.example .env. Every developer has a consistent local environment with minimal setup.
Dotenv Learning Path
flowchart LR
A[Environment Variables] --> B[Dotenv]
B --> C[.env File Format]
B --> D[Multiple Files]
B --> E[Security]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Basic Dotenv Usage
Dotenv loads variables from a .env file at application startup.
# .env
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_dev
DB_USER=dev_user
DB_PASSWORD=dev_password
LOG_LEVEL=debug
CACHE_TTL=300
ALLOWED_ORIGINS=http://localhost:5173
// index.js
require("dotenv").config();
const config = {
port: parseInt(process.env.PORT),
dbHost: process.env.DB_HOST,
dbName: process.env.DB_NAME,
logLevel: process.env.LOG_LEVEL
};
console.log("Configuration loaded from .env");
console.log("Port:", config.port);
console.log("Database:", config.dbHost + "/" + config.dbName);
// Configuration loaded from .env
// Port: 3000
// Database: localhost/myapp_dev
.env File Syntax
The .env file format supports comments, multiline values, and variable expansion.
# This is a comment
APP_NAME=MyApp
# Simple values
PORT=3000
HOST=0.0.0.0
# Quoted values (preserve spaces)
GREETING="Hello World"
# Multiline values (use quotes with newlines)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"
# Variable expansion
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
# Empty values
OPTIONAL_FEATURE=
# Escaped values
SPECIAL_CHARS=password\#123
const dotenv = require("dotenv");
const result = dotenv.config();
if (result.error) {
console.error("Failed to load .env:", result.error.message);
} else {
console.log("Parsed", Object.keys(result.parsed).length, "variables");
console.log("APP_NAME:", result.parsed.APP_NAME);
}
Multiple Environment Files
Different environments need different .env files.
// .env.development (default for local dev)
// NODE_ENV=development
// DB_HOST=localhost
// LOG_LEVEL=debug
// .env.staging
// NODE_ENV=staging
// DB_HOST=staging-db.internal
// LOG_LEVEL=info
// .env.production
// NODE_ENV=production
// DB_HOST=prod-db.amazonaws.com
// LOG_LEVEL=warn
const path = require("path");
const dotenv = require("dotenv");
function loadEnvConfig() {
const environment = process.env.NODE_ENV || "development";
const envFile = path.resolve(process.cwd(), `.env.${environment}`);
const result = dotenv.config({ path: envFile });
if (result.error) {
console.warn(`No .env.${environment} file found, using .env`);
dotenv.config();
} else {
console.log(`Loaded configuration from .env.${environment}`);
}
}
loadEnvConfig();
.env.example Template
Maintain a .env.example file as documentation for required variables.
# .env.example - Copy this to .env and fill in your values
# Application
PORT=3000
NODE_ENV=development
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_dev
DB_USER=postgres
# DB_PASSWORD= (no default, required for production)
# Authentication
JWT_SECRET=change-me-in-production
JWT_EXPIRES_IN=7d
# External Services
REDIS_URL=redis://localhost:6379
API_KEY=your-api-key-here
# Feature Flags
ENABLE_CACHE=true
ENABLE_ANALYTICS=false
function validateEnvExample() {
const fs = require("fs");
const path = require("path");
const examplePath = path.join(__dirname, ".env.example");
const envPath = path.join(__dirname, ".env");
if (!fs.existsSync(envPath)) {
console.log("No .env file found.");
console.log(`Copy .env.example to .env:`);
console.log(` cp .env.example .env`);
return false;
}
const exampleVars = fs.readFileSync(examplePath, "utf8")
.split("\n")
.filter(l => l.includes("=") && !l.startsWith("#"))
.map(l => l.split("=")[0].trim());
const envVars = fs.readFileSync(envPath, "utf8")
.split("\n")
.filter(l => l.includes("=") && !l.startsWith("#"))
.map(l => l.split("=")[0].trim());
const missing = exampleVars.filter(v => !envVars.includes(v));
if (missing.length > 0) {
console.warn("Missing variables in .env:", missing.join(", "));
} else {
console.log("All required environment variables present");
}
}
validateEnvExample();
Security Considerations
.env files contain secrets and must be protected.
// .gitignore
.env
.env.*.local
.env.production
// Only commit:
// .env.example
// .env.development (without real secrets)
class DotenvSecurity {
static checkRisks() {
const risks = [
{
risk: "Committing .env to git",
mitigation: "Add .env to .gitignore immediately",
severity: "critical"
},
{
risk: "Using .env in production",
mitigation: "Use orchestrator-injected env vars or secrets manager",
severity: "high"
},
{
risk: "Sharing .env via chat or email",
mitigation: "Use a secrets management tool for team sharing",
severity: "high"
},
{
risk: "Including .env in Docker images",
mitigation: "Use build args or runtime env vars",
severity: "critical"
}
];
risks.forEach(r => {
console.log(`[${r.severity.toUpperCase()}] ${r.risk}`);
console.log(` Mitigation: ${r.mitigation}`);
});
}
}
DotenvSecurity.checkRisks();
// [CRITICAL] Committing .env to git
// Mitigation: Add .env to .gitignore immediately
// [HIGH] Using .env in production
// Mitigation: Use orchestrator-injected env vars or secrets manager
Common Mistakes
Committing .env to version control -- This exposes secrets to everyone with repo access. Add .env to .gitignore immediately.
Using dotenv in production -- Dotenv is a development convenience. In production, set environment variables through your deployment platform.
Not using .env.example -- Without an example file, new developers don't know which variables to set. Always maintain a .env.example.
Hardcoding fallback values for secrets -- process.env.DB_PASSWORD || "password" creates a security risk. Required secrets should fail at startup if missing.
Loading dotenv after importing modules that need env vars -- Dotenv must be loaded before any module that reads process.env. Require it at the very top of the entry point.
Practice Questions
What is the purpose of .env.example? To document all required environment variables without committing actual secrets. Developers copy it to .env and fill in values.
Why should you not use dotenv in production? Production environments should inject environment variables through the deployment platform (Kubernetes, Docker, cloud provider) for security and auditability.
How do you handle different configurations for different environments? Use separate .env files (.env.development, .env.staging, .env.production) and load the appropriate one based on NODE_ENV.
Challenge: Implement a dotenv loader that supports variable expansion and type coercion.
class DotenvLoader {
constructor() {
this.vars = {};
}
parse(content) {
const lines = content.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIndex = trimmed.indexOf("=");
if (eqIndex === -1) continue;
const key = trimmed.slice(0, eqIndex).trim();
let value = trimmed.slice(eqIndex + 1).trim();
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
this.vars[key] = value;
}
// Variable expansion
for (const [key, value] of Object.entries(this.vars)) {
this.vars[key] = value.replace(/\${([^}]+)}/g, (_, name) => {
return this.vars[name] || process.env[name] || "";
});
}
return this.vars;
}
}
const loader = new DotenvLoader();
const vars = loader.parse("DB_HOST=localhost\nDATABASE_URL=postgresql://${DB_HOST}:5432/db");
console.log("DATABASE_URL:", vars.DATABASE_URL);
// DATABASE_URL: postgresql://localhost:5432/db
FAQ
Mini Project
Build a dotenv configuration loader that supports multiple environment files, variable expansion, type coercion, and validation against a schema defined in code.
class CompleteDotenvManager {
constructor(options = {}) {
this.path = options.path || process.cwd();
this.env = options.env || process.env.NODE_ENV || "development";
}
load() {
const dotenv = require("dotenv");
const path = require("path");
const files = [
path.join(this.path, ".env"),
path.join(this.path, `.env.${this.env}`),
path.join(this.path, ".env.local"),
path.join(this.path, `.env.${this.env}.local`)
];
files.forEach(file => {
const result = dotenv.config({ path: file });
if (result.error) return;
console.log(`Loaded: ${path.basename(file)}`);
});
return process.env;
}
}
const mgr = new CompleteDotenvManager({ env: "development" });
mgr.load();
console.log("Dotenv configuration complete");
What's Next
Now that you understand dotenv, learn about configuration file formats like YAML, JSON, and TOML. Then explore YAML configuration in depth.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro