Skip to content

Dotenv — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

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

  1. Committing .env to version control -- This exposes secrets to everyone with repo access. Add .env to .gitignore immediately.

  2. Using dotenv in production -- Dotenv is a development convenience. In production, set environment variables through your deployment platform.

  3. Not using .env.example -- Without an example file, new developers don't know which variables to set. Always maintain a .env.example.

  4. Hardcoding fallback values for secrets -- process.env.DB_PASSWORD || "password" creates a security risk. Required secrets should fail at startup if missing.

  5. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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

What is the difference between .env, .env.local, and .env.development?

.env is loaded by default. .env.local overrides .env for local overrides. .env.development is loaded when NODE_ENV=development.

Can I use dotenv with TypeScript?

Yes. Import dotenv at the top of your entry file before any other imports. Use dotenv.config() as usual.

How do I handle .env files in Docker?

Use the --env-file flag with docker run, or the env_file section in docker-compose.yml. Don't bake .env into the image.

Should I validate .env file content?

Yes, validate at application startup. Check that all required variables are present and correctly formatted.

What is the order of precedence when multiple .env files exist?

dotenv loads files in order: .env, .env.local, .env.{NODE_ENV}, .env.{NODE_ENV}.local. Later files override earlier ones.

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