Node.js 환경변수 관리 - dotenv에서 docker secrets까지
문제 상황
신규 프로젝트에 개발/스테이징/프로덕션 환경이 추가되면서 환경변수 관리가 복잡해졌다. 특히 API 키와 DB 접속 정보가 환경별로 다르고, 팀원들마다 로컬 설정이 달라 매번 문의가 들어왔다.
로컬 개발 환경
기본적으로 dotenv를 사용했다. .env.example 파일로 필요한 환경변수 목록을 관리했다.
// config.js
require('dotenv').config();
const config = {
port: process.env.PORT || 3000,
database: {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
},
};
// 필수 환경변수 검증
const required = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'JWT_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
module.exports = config;
앱 시작 시점에 필수 환경변수를 검증하는 로직을 추가했더니 설정 누락으로 인한 런타임 에러가 크게 줄었다.
Docker 환경
Docker Compose에서는 env_file을 사용했다.
# docker-compose.yml
version: '3.8'
services:
app:
build: .
env_file:
- .env.development
ports:
- "3000:3000"
프로덕션에서는 민감한 정보를 Docker secrets로 관리하도록 변경했다. Swarm 모드를 사용 중이라 secrets API를 활용했다.
const fs = require('fs');
function getSecret(secretName) {
try {
return fs.readFileSync(`/run/secrets/${secretName}`, 'utf8').trim();
} catch (err) {
return process.env[secretName];
}
}
const dbPassword = getSecret('DB_PASSWORD');
CI/CD 파이프라인
GitHub Actions에서는 Repository Secrets를 사용했다. 환경별로 분리하기 위해 Environment를 활용했다.
# .github/workflows/deploy.yml
jobs:
deploy-production:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v3
- name: Deploy
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: |
npm run deploy
정리
환경변수 관리는 단순해 보이지만 환경이 늘어날수록 복잡해진다. 필수 환경변수 검증, .env.example 문서화, 환경별 분리를 체계화하니 온보딩 시간이 줄고 설정 관련 이슈가 거의 사라졌다.