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:
- Never commit .env files
- Provide .env.example
- Validate required variables
- Use platform vars in production
- Rotate secrets regularly
Next Steps:
- Build with Node.js
- Deploy with Docker
- Secure with Security Guide
Resources: