dotenv Complete Guide | Environment Variables in Node.js

dotenv Complete Guide | Environment Variables in Node.js

이 글의 핵심

dotenv loads environment variables from a .env file into process.env. It's the standard way to manage configuration and secrets in Node.js applications.

Introduction

dotenv is a zero-dependency module that loads environment variables from a .env file into process.env. It’s the standard for managing configuration in Node.js apps.

The Problem

Hardcoded values (bad):

const db = mysql.createConnection({
  host: 'localhost',
  user: 'admin',
  password: 'secret123', // ❌ Exposed in code!
  database: 'myapp',
});

With dotenv (good):

require('dotenv').config();

const db = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD, // ✅ Secret not in code
  database: process.env.DB_NAME,
});

1. Installation

npm install dotenv

2. Basic Usage

Create .env file:

# .env
DB_HOST=localhost
DB_USER=admin
DB_PASSWORD=secret123
DB_NAME=myapp
PORT=3000

Load in your app:

// Load as early as possible
require('dotenv').config();

// Now use environment variables
console.log(process.env.DB_HOST);     // 'localhost'
console.log(process.env.PORT);        // '3000'

3. ES Modules

import 'dotenv/config';

// Or
import dotenv from 'dotenv';
dotenv.config();

console.log(process.env.DB_HOST);

4. Custom Path

require('dotenv').config({ path: '/custom/path/.env' });

// Or multiple files
require('dotenv').config({ path: '.env.local' });
require('dotenv').config({ path: '.env' });

5. Multi-Environment Setup

Development

# .env.development
NODE_ENV=development
DB_HOST=localhost
DB_PORT=5432
API_URL=http://localhost:3000
LOG_LEVEL=debug

Production

# .env.production
NODE_ENV=production
DB_HOST=prod-db.example.com
DB_PORT=5432
API_URL=https://api.example.com
LOG_LEVEL=error

Load Based on Environment

const path = require('path');
const dotenv = require('dotenv');

const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
dotenv.config({ path: path.resolve(process.cwd(), envFile) });

console.log(`Running in ${process.env.NODE_ENV} mode`);

6. Variable Types

String Values

APP_NAME=MyApp
API_KEY=abc123xyz
console.log(process.env.APP_NAME); // 'MyApp'

Numbers

PORT=3000
MAX_CONNECTIONS=100
// Convert to number
const port = parseInt(process.env.PORT, 10);
const maxConn = Number(process.env.MAX_CONNECTIONS);

Boolean Values

DEBUG=true
ENABLE_CACHE=false
const debug = process.env.DEBUG === 'true';
const enableCache = process.env.ENABLE_CACHE === 'true';

JSON Values

# Not recommended, but possible
CONFIG_JSON='{"key":"value","nested":{"prop":true}}'
const config = JSON.parse(process.env.CONFIG_JSON);

7. Default Values

const {
  PORT = 3000,
  DB_HOST = 'localhost',
  NODE_ENV = 'development',
} = process.env;

console.log(PORT); // Uses 3000 if PORT not set

8. Validation

Manual Validation

require('dotenv').config();

const requiredEnvVars = [
  'DB_HOST',
  'DB_USER',
  'DB_PASSWORD',
  'JWT_SECRET',
];

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`);
  }
}

With envalid

npm install envalid
require('dotenv').config();
const { str, port, num, bool } = require('envalid');

const env = require('envalid').cleanEnv(process.env, {
  NODE_ENV: str({ choices: ['development', 'test', 'production'] }),
  PORT: port({ default: 3000 }),
  DB_HOST: str(),
  DB_PORT: num({ default: 5432 }),
  ENABLE_HTTPS: bool({ default: false }),
});

console.log(env.PORT); // Validated and converted to number

9. Security Best Practices

1. Never Commit .env

# .gitignore
.env
.env.local
.env.*.local

2. Provide .env.example

# .env.example (commit this!)
DB_HOST=localhost
DB_USER=your_db_user
DB_PASSWORD=your_db_password
JWT_SECRET=your_secret_key
PORT=3000

3. Rotate Secrets Regularly

# Update secrets periodically
JWT_SECRET=new_secret_$(date +%s)

4. Use Strong Secrets

// Generate strong secret
const crypto = require('crypto');
const secret = crypto.randomBytes(64).toString('hex');
console.log(secret);

5. Restrict File Permissions

chmod 600 .env  # Only owner can read/write

10. Production Deployment

Docker

# Dockerfile
FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Don't copy .env - use environment variables
CMD ["node", "index.js"]
# docker-compose.yml
version: '3'
services:
  app:
    build: .
    environment:
      DB_HOST: postgres
      DB_USER: admin
      DB_PASSWORD: ${DB_PASSWORD}
    env_file:
      - .env

Heroku

# Set via CLI
heroku config:set DB_HOST=postgres.heroku.com
heroku config:set DB_PASSWORD=secret

# Or via dashboard
# Settings > Config Vars

Vercel

# .vercel.json (don't commit secrets!)
# Or use Vercel dashboard

vercel env add DB_PASSWORD

AWS/GCP

Use secrets managers:

const { SecretsManagerClient } = require('@aws-sdk/client-secrets-manager');

async function getSecret(secretName) {
  const client = new SecretsManagerClient({ region: 'us-east-1' });
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: secretName })
  );
  return JSON.parse(response.SecretString);
}

11. Real-World Example

// config.js
require('dotenv').config();

const config = {
  app: {
    name: process.env.APP_NAME || 'MyApp',
    port: parseInt(process.env.PORT, 10) || 3000,
    env: process.env.NODE_ENV || 'development',
  },
  db: {
    host: process.env.DB_HOST,
    port: parseInt(process.env.DB_PORT, 10) || 5432,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    name: process.env.DB_NAME,
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '1h',
  },
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT, 10) || 6379,
  },
  email: {
    host: process.env.EMAIL_HOST,
    port: parseInt(process.env.EMAIL_PORT, 10) || 587,
    user: process.env.EMAIL_USER,
    password: process.env.EMAIL_PASSWORD,
  },
};

// Validate critical vars
if (!config.jwt.secret) {
  throw new Error('JWT_SECRET is required');
}

module.exports = config;
// app.js
const config = require('./config');
const express = require('express');

const app = express();

app.listen(config.app.port, () => {
  console.log(`${config.app.name} running on port ${config.app.port}`);
});

12. TypeScript Integration

// env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: 'development' | 'production' | 'test';
      PORT: string;
      DB_HOST: string;
      DB_USER: string;
      DB_PASSWORD: string;
      JWT_SECRET: string;
    }
  }
}

export {};
// config.ts
import 'dotenv/config';

interface Config {
  port: number;
  database: {
    host: string;
    user: string;
    password: string;
  };
}

const config: Config = {
  port: parseInt(process.env.PORT, 10),
  database: {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
  },
};

export default config;

13. Troubleshooting

Variables Not Loading

// Debug mode
require('dotenv').config({ debug: true });

// Check if .env exists
const fs = require('fs');
if (!fs.existsSync('.env')) {
  console.error('.env file not found!');
}

Override Existing Variables

// Force override (not recommended)
require('dotenv').config({ override: true });

Encoding Issues

// Specify encoding
require('dotenv').config({ encoding: 'utf8' });

Summary

dotenv simplifies environment management:

  • Keep secrets out of code
  • Different configs per environment
  • Easy local development
  • Production-ready
  • Zero dependencies

Key Takeaways:

  1. Never commit .env files
  2. Provide .env.example
  3. Validate required variables
  4. Use platform vars in production
  5. Rotate secrets regularly

Next Steps:

  • Build with Node.js
  • Deploy with Docker
  • Secure with Security Guide

Resources: