로컬에서 node server.js 잘 돌면 끝일 것 같지만 — production 은 그것만으론 부족하다. 서버가 죽으면? 메모리 leak 누적? 새 코드 배포 시 다운타임? 8 코어 활용? 직접 다 해결하려면 일주일.
표준 답은 두 가지 — PM2(전통적 VM/베어메탈 환경) 또는 Docker(컨테이너 오케스트레이션). 둘 다 배워두면 어떤 회사 환경이든 적응 가능. 이번이 마지막 인프라 챕터.
1. PM2 — 프로세스 매니저 표준
"node server.js 의 production 버전"
$ npm install -g pm2
# 기본 실행
$ pm2 start server.js --name api
# cluster 모드 (23편 + 모든 CPU 코어)
$ pm2 start server.js --name api -i max
# 환경변수 + watch
$ pm2 start server.js -i max --env production
이 명령 하나가 23편 cluster 모듈을 자동 적용 + 죽으면 재시작 + 로그 통합. 명령 5개로 일상 운영.
| 명령 | 역할 |
pm2 list | 실행 중 프로세스 + CPU·메모리 |
pm2 logs api | 실시간 로그 (tail) |
pm2 reload api | 무중단 재배포 (워커 순차 교체) |
pm2 stop api / delete | 멈춤·완전 삭제 |
pm2 startup + save | 서버 부팅 시 자동 시작 등록 |
pm2 reload 가 진짜 강력 — 워커 1개씩 새 코드로 교체. 다운타임 0. 결제·API 처럼 절대 끊기면 안 되는 서비스에 필수.
2. ecosystem.config.js — 설정 한 파일로
옵션이 많아지면 명령보다 파일.
// ecosystem.config.cjs
module.exports = {
apps: [{
name: 'api',
script: 'server.js',
instances: 'max', // 모든 CPU
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
max_memory_restart: '500M', // 메모리 leak 보호
error_file: '/var/log/api-err.log',
out_file: '/var/log/api-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
}],
};
// 실행
// $ pm2 start ecosystem.config.cjs
max_memory_restart 가 자주 쓰임 — 메모리가 임계점 넘으면 자동 재시작. 잠재적 leak 대응.
3. Docker — 컨테이너 표준
PM2 가 "내 서버 위에서 잘 돌게" 라면, Docker 는 "어디서나 똑같이 돌게". 의존성 + Node 버전 + OS 라이브러리까지 한 이미지로.
# Dockerfile (multi-stage, 권장)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
USER node # ★ root 로 실행 금지
CMD ["node", "server.js"]
핵심 두 가지 — multi-stage 빌드(빌드 캐시 + 최종 이미지 크기 감소) + USER node(보안, root 실행 금지).
$ docker build -t my-api .
$ docker run -p 3000:3000 --env-file .env.production my-api
.dockerignore 필수
# .dockerignore
node_modules
.git
.env
.env.local
*.log
coverage
.next
.vscode
없으면 node_modules + .git 까지 이미지에 포함되어 수백 MB. .gitignore 와 거의 같이.
4. docker-compose — 멀티 컨테이너 묶음
API + DB + Redis 같은 묶음을 한 파일로.
# docker-compose.yml
services:
api:
build: .
ports: ['3000:3000']
env_file: .env.production
depends_on: [db, redis]
restart: unless-stopped
db:
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- dbdata:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
dbdata:
$ docker compose up -d # 백그라운드 시작
$ docker compose logs -f api # 로그
$ docker compose down # 정지
로컬 개발도 production 도 같은 파일. "내 컴에선 되는데요" 가 거의 사라진다.
5. 12-factor 체크리스트
production 코드 배포 전 점검. 12-factor app 의 핵심 5개만 추려보면:
| 원칙 | 체크 |
| 설정은 환경변수 | 17편 dotenv·Zod 검증 |
| 로그는 stdout | 21편 winston Console transport |
| 상태 없는 프로세스 | 세션은 DB·Redis, 파일은 S3 |
| graceful shutdown | SIGTERM 처리, in-flight 요청 끝까지 |
| 한 단계 한 명령 | npm start 한 줄로 시작 |
graceful shutdown — 자주 빠뜨림
// server.js
const server = app.listen(3000);
const shutdown = (signal) => {
console.log(`${signal} 수신, graceful 종료 시작`);
server.close(() => {
console.log('HTTP 서버 종료');
db.end(); // DB 풀 닫기
process.exit(0);
});
// 10초 안에 안 끝나면 강제 종료
setTimeout(() => process.exit(1), 10000);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
없으면 PM2·Docker 재시작 시 진행 중이던 요청이 통째로 끊김. 결제 같은 곳에선 사고.
오늘날 현실 — Kubernetes·서버리스도 알아둘 것 — 작은 회사·사이드는 PM2 + 1 서버, 중규모는 Docker + docker-compose 또는 ECS/Cloud Run, 대규모는 Kubernetes(EKS/GKE)·Vercel·Cloudflare Workers. PM2·Docker 가 그 위로 가는 발판. 이 둘만 단단해도 신입 백엔드 인터뷰 90% 통과.
요약 — 25편 좌표
여기까지 정리. PM2 — 단일 서버에서 cluster + 자동 재시작 + 무중단 재배포 (pm2 reload) + 자동 부팅. Docker — multi-stage + USER node + .dockerignore. docker-compose 로 API+DB+Redis 묶음. graceful shutdown(SIGTERM) 빠뜨리지 말 것. 12-factor 5개 체크리스트가 production 준비도 측정. 다음 편은 시리즈 마지막 — 실전 Todo API 미니 프로젝트로 25편 전체 합치기.
다음 편 예고 — 실전 미니 프로젝트 (완결)
Todo API 처음부터 끝까지 — 25편 모든 개념 결합. 26편.