Node.js 배포 가이드 | PM2, Docker, AWS, Nginx

Node.js 배포 가이드 | PM2, Docker, AWS, Nginx

이 글의 핵심

Node.js 배포 가이드에 대한 실전 가이드입니다. PM2, Docker, AWS, Nginx 등을 예제와 함께 상세히 설명합니다.

들어가며

배포 체크리스트

로컬에서 node app.js로 돌리던 것을 프로세스 관리(PM2)·리버스 프록시(Nginx)·컨테이너(Docker)까지 올리면, 재시작·로그·환경 분리·무중단 배포 같은 운영 요구를 맞출 수 있습니다. 아래 체크리스트는 “코드만 올리는 것”이 아니라 실행 환경까지 포함했을 때의 최소 점검 항목입니다.

배포 전에는 Node.js 테스트(Jest 등)로 회귀를 막고, GitHub Actions CI/CD로 빌드·테스트를 자동화하는 흐름이 흔합니다. 로컬·스테이징은 Docker Compose, 오케스트레이션 입문은 minikube, C++·네이티브 쪽은 C++ Docker·배포 이미지·C++ GitHub Actions와 같은 언어 무관 패턴을 비교해 보세요.

배포 전 확인사항:

  • 환경 변수 설정
  • 프로덕션 의존성만 설치
  • 에러 로깅 설정
  • 보안 설정 (Helmet, CORS)
  • 데이터베이스 마이그레이션
  • 정적 파일 빌드
  • 테스트 통과
  • 성능 테스트

배포 방식:

  • 전통적 방식: VPS, PM2, Nginx
  • 컨테이너: Docker, Kubernetes
  • 서버리스: AWS Lambda, Vercel
  • PaaS: Heroku, Railway, Render

1. PM2 (Process Manager)

설치

# 전역 설치
npm install -g pm2

기본 사용법

# 앱 시작
pm2 start app.js

# 이름 지정
pm2 start app.js --name "my-app"

# 환경 변수 설정
pm2 start app.js --name "my-app" --env production

# Watch 모드 (파일 변경 시 재시작)
pm2 start app.js --watch

# 인터프리터 지정
pm2 start app.js --interpreter node

클러스터 모드

# 클러스터 모드 (멀티 코어 활용)
pm2 start app.js -i max  # CPU 코어 수만큼

# 특정 개수
pm2 start app.js -i 4

# 무중단 재시작
pm2 reload my-app

PM2 명령어

# 상태 확인
pm2 status
pm2 list

# 로그 확인
pm2 logs
pm2 logs my-app
pm2 logs --lines 100

# 모니터링
pm2 monit

# 재시작
pm2 restart my-app
pm2 restart all

# 중지
pm2 stop my-app
pm2 stop all

# 삭제
pm2 delete my-app
pm2 delete all

# 정보
pm2 info my-app

# 저장 (현재 프로세스 목록)
pm2 save

# 부팅 시 자동 시작
pm2 startup
pm2 save

ecosystem.config.js

// ecosystem.config.js
module.exports = {
    apps: [{
        name: 'my-app',
        script: './app.js',
        instances: 'max',
        exec_mode: 'cluster',
        env: {
            NODE_ENV: 'development',
            PORT: 3000
        },
        env_production: {
            NODE_ENV: 'production',
            PORT: 8080
        },
        error_file: './logs/err.log',
        out_file: './logs/out.log',
        log_date_format: 'YYYY-MM-DD HH:mm:ss',
        merge_logs: true,
        max_memory_restart: '500M',
        watch: false,
        ignore_watch: ['node_modules', 'logs'],
        max_restarts: 10,
        min_uptime: '10s'
    }]
};

사용:

# 시작
pm2 start ecosystem.config.js

# 프로덕션 환경
pm2 start ecosystem.config.js --env production

# 재시작
pm2 restart ecosystem.config.js

2. Docker

Dockerfile

# Dockerfile
FROM node:20-alpine

# 작업 디렉토리
WORKDIR /app

# 의존성 파일 복사
COPY package*.json ./

# 의존성 설치
RUN npm ci --only=production

# 소스 코드 복사
COPY . .

# 포트 노출
EXPOSE 3000

# 헬스체크
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD node healthcheck.js

# 앱 시작
CMD ["node", "app.js"]

.dockerignore

node_modules
npm-debug.log
.env
.git
.gitignore
README.md
.vscode
coverage
.DS_Store

Docker 명령어

# 이미지 빌드
docker build -t my-app:1.0.0 .

# 컨테이너 실행
docker run -d \
    --name my-app \
    -p 3000:3000 \
    -e NODE_ENV=production \
    -e PORT=3000 \
    my-app:1.0.0

# 로그 확인
docker logs my-app
docker logs -f my-app  # 실시간

# 컨테이너 중지/시작
docker stop my-app
docker start my-app

# 컨테이너 재시작
docker restart my-app

# 컨테이너 삭제
docker rm my-app

# 이미지 삭제
docker rmi my-app:1.0.0

Docker Compose

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
      - MONGODB_URI=mongodb://mongo:27017/mydb
    depends_on:
      - mongo
    restart: unless-stopped
    volumes:
      - ./logs:/app/logs
  
  mongo:
    image: mongo:7
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db
    restart: unless-stopped
  
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    restart: unless-stopped

volumes:
  mongo-data:

실행:

# 시작
docker-compose up -d

# 로그
docker-compose logs -f

# 중지
docker-compose down

# 재시작
docker-compose restart

# 빌드 후 시작
docker-compose up -d --build

3. Nginx 리버스 프록시

설치 (Ubuntu)

sudo apt update
sudo apt install nginx

기본 설정

# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

활성화:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

SSL (Let’s Encrypt)

# Certbot 설치
sudo apt install certbot python3-certbot-nginx

# SSL 인증서 발급
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

# 자동 갱신 테스트
sudo certbot renew --dry-run

SSL 설정:

server {
    listen 443 ssl http2;
    server_name yourdomain.com;
    
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    location / {
        proxy_pass http://localhost:3000;
        # ... 프록시 설정
    }
}

# HTTP → HTTPS 리다이렉트
server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

로드 밸런싱

# upstream 정의
upstream backend {
    least_conn;  # 연결 수가 적은 서버로
    server localhost:3000;
    server localhost:3001;
    server localhost:3002;
}

server {
    listen 80;
    server_name yourdomain.com;
    
    location / {
        proxy_pass http://backend;
        # ... 프록시 설정
    }
}

4. AWS 배포

EC2 배포

1. EC2 인스턴스 생성:

  • Ubuntu Server 선택
  • 보안 그룹: HTTP(80), HTTPS(443), SSH(22) 포트 열기

2. 서버 설정:

# SSH 접속
ssh -i your-key.pem ubuntu@your-ec2-ip

# Node.js 설치
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

# Git 설치
sudo apt-get install git

# 프로젝트 클론
git clone https://github.com/yourusername/your-repo.git
cd your-repo

# 의존성 설치
npm ci --only=production

# 환경 변수 설정
nano .env

# PM2로 실행
npm install -g pm2
pm2 start app.js --name "my-app" -i max
pm2 startup
pm2 save

# Nginx 설정
sudo apt install nginx
sudo nano /etc/nginx/sites-available/myapp
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Elastic Beanstalk

설치:

pip install awsebcli

초기화:

eb init

# 환경 생성 및 배포
eb create production
eb deploy

# 로그 확인
eb logs

# 환경 변수 설정
eb setenv NODE_ENV=production PORT=8080

# 상태 확인
eb status

# 종료
eb terminate production

Lambda (서버리스)

// lambda.js
const serverless = require('serverless-http');
const app = require('./app');

module.exports.handler = serverless(app);

serverless.yml:

service: my-app

provider:
  name: aws
  runtime: nodejs20.x
  region: ap-northeast-2

functions:
  app:
    handler: lambda.handler
    events:
      - http:
          path: /{proxy+}
          method: ANY
          cors: true

배포:

npm install -g serverless
serverless deploy

5. 환경 변수 관리

.env 파일

# .env.development
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/mydb-dev
JWT_SECRET=dev-secret

# .env.production
NODE_ENV=production
PORT=8080
MONGODB_URI=mongodb://prod-server:27017/mydb
JWT_SECRET=super-secret-production-key

dotenv 사용

// config.js
require('dotenv').config({
    path: `.env.${process.env.NODE_ENV || 'development'}`
});

const config = {
    nodeEnv: process.env.NODE_ENV || 'development',
    port: process.env.PORT || 3000,
    mongodbUri: process.env.MONGODB_URI,
    jwtSecret: process.env.JWT_SECRET,
    
    isDevelopment: process.env.NODE_ENV === 'development',
    isProduction: process.env.NODE_ENV === 'production',
    isTest: process.env.NODE_ENV === 'test'
};

// 필수 환경 변수 검증
const required = ['MONGODB_URI', 'JWT_SECRET'];
for (const key of required) {
    if (!process.env[key]) {
        throw new Error(`환경 변수 ${key}가 필요합니다`);
    }
}

module.exports = config;

AWS Systems Manager Parameter Store

# AWS CLI로 환경 변수 저장
aws ssm put-parameter \
    --name "/myapp/production/JWT_SECRET" \
    --value "your-secret-key" \
    --type "SecureString"
// config/aws.js
const AWS = require('aws-sdk');
const ssm = new AWS.SSM({ region: 'ap-northeast-2' });

async function loadConfig() {
    const params = {
        Names: [
            '/myapp/production/JWT_SECRET',
            '/myapp/production/MONGODB_URI'
        ],
        WithDecryption: true
    };
    
    const result = await ssm.getParameters(params).promise();
    
    const config = {};
    result.Parameters.forEach(param => {
        const key = param.Name.split('/').pop();
        config[key] = param.Value;
    });
    
    return config;
}

module.exports = { loadConfig };

6. 로깅

Winston

npm install winston
// logger.js
const winston = require('winston');

const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
    ),
    defaultMeta: { service: 'my-app' },
    transports: [
        // 파일 로그
        new winston.transports.File({
            filename: 'logs/error.log',
            level: 'error'
        }),
        new winston.transports.File({
            filename: 'logs/combined.log'
        })
    ]
});

// 개발 환경에서는 콘솔 출력
if (process.env.NODE_ENV !== 'production') {
    logger.add(new winston.transports.Console({
        format: winston.format.combine(
            winston.format.colorize(),
            winston.format.simple()
        )
    }));
}

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

logger.info('서버 시작', { port: 3000 });
logger.error('에러 발생', { error: err.message, stack: err.stack });
logger.warn('경고', { memory: process.memoryUsage() });

Morgan (HTTP 로깅)

const morgan = require('morgan');
const logger = require('./logger');

// 커스텀 스트림
const stream = {
    write: (message) => {
        logger.info(message.trim());
    }
};

// 프로덕션
if (process.env.NODE_ENV === 'production') {
    app.use(morgan('combined', { stream }));
} else {
    app.use(morgan('dev'));
}

7. 모니터링

PM2 모니터링

# 실시간 모니터링
pm2 monit

# 웹 대시보드
pm2 plus

헬스체크

// healthcheck.js
const http = require('http');

const options = {
    host: 'localhost',
    port: 3000,
    path: '/health',
    timeout: 2000
};

const request = http.request(options, (res) => {
    if (res.statusCode === 200) {
        process.exit(0);
    } else {
        process.exit(1);
    }
});

request.on('error', () => {
    process.exit(1);
});

request.end();
// app.js
app.get('/health', (req, res) => {
    res.status(200).json({
        status: 'ok',
        uptime: process.uptime(),
        timestamp: Date.now()
    });
});

메트릭 수집

npm install prom-client
const promClient = require('prom-client');

// 기본 메트릭 수집
const register = new promClient.Registry();
promClient.collectDefaultMetrics({ register });

// 커스텀 메트릭
const httpRequestDuration = new promClient.Histogram({
    name: 'http_request_duration_seconds',
    help: 'HTTP 요청 처리 시간',
    labelNames: ['method', 'route', 'status_code'],
    registers: [register]
});

// 미들웨어
app.use((req, res, next) => {
    const start = Date.now();
    
    res.on('finish', () => {
        const duration = (Date.now() - start) / 1000;
        httpRequestDuration
            .labels(req.method, req.route?.path || req.path, res.statusCode)
            .observe(duration);
    });
    
    next();
});

// 메트릭 엔드포인트
app.get('/metrics', async (req, res) => {
    res.set('Content-Type', register.contentType);
    res.end(await register.metrics());
});

8. CI/CD

GitHub Actions

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests
        run: npm test
      
      - name: Run linter
        run: npm run lint
  
  deploy:
    needs: test
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            cd /home/ubuntu/my-app
            git pull origin main
            npm ci --only=production
            pm2 reload ecosystem.config.js --env production

Docker 이미지 빌드 및 배포

# .github/workflows/docker.yml
name: Build and Deploy Docker

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: yourusername/my-app:latest
      
      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            docker pull yourusername/my-app:latest
            docker stop my-app || true
            docker rm my-app || true
            docker run -d \
              --name my-app \
              -p 3000:3000 \
              -e NODE_ENV=production \
              yourusername/my-app:latest

9. 무중단 배포

PM2 무중단 재시작

# reload: 무중단 재시작 (클러스터 모드)
pm2 reload my-app

# gracefulReload: 더 안전한 재시작
pm2 gracefulReload my-app

Graceful Shutdown

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

const server = app.listen(3000);

// 진행 중인 요청 추적
let connections = new Set();

server.on('connection', (conn) => {
    connections.add(conn);
    
    conn.on('close', () => {
        connections.delete(conn);
    });
});

// Graceful Shutdown
function gracefulShutdown(signal) {
    console.log(`${signal} 신호 받음. 서버 종료 중...`);
    
    // 새 연결 거부
    server.close(async () => {
        console.log('서버 종료됨');
        
        // 데이터베이스 연결 종료
        await mongoose.connection.close();
        
        process.exit(0);
    });
    
    // 30초 후 강제 종료
    setTimeout(() => {
        console.error('강제 종료');
        process.exit(1);
    }, 30000);
    
    // 기존 연결 종료
    connections.forEach((conn) => {
        conn.end();
        
        setTimeout(() => {
            conn.destroy();
        }, 5000);
    });
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

10. 자주 발생하는 문제

문제 1: 포트 충돌

에러:

Error: listen EADDRINUSE: address already in use :::3000

해결:

# 프로세스 찾기
lsof -i :3000
netstat -ano | findstr :3000

# 프로세스 종료
kill -9 <PID>
taskkill /PID <PID> /F

# PM2로 관리
pm2 delete all
pm2 start app.js

문제 2: 메모리 누수

증상: 메모리 사용량이 계속 증가

해결:

// PM2 설정
module.exports = {
    apps: [{
        name: 'my-app',
        script: './app.js',
        max_memory_restart: '500M'  // 500MB 초과 시 재시작
    }]
};

// 메모리 모니터링
setInterval(() => {
    const used = process.memoryUsage();
    console.log({
        rss: `${Math.round(used.rss / 1024 / 1024)} MB`,
        heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`,
        heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`
    });
}, 60000);

문제 3: 환경 변수 누락

// 시작 시 검증
const required = [
    'NODE_ENV',
    'PORT',
    'MONGODB_URI',
    'JWT_SECRET'
];

for (const key of required) {
    if (!process.env[key]) {
        console.error(`환경 변수 ${key}가 설정되지 않았습니다`);
        process.exit(1);
    }
}

11. 실전 팁

배포 스크립트

{
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js",
    "test": "jest",
    "lint": "eslint .",
    "build": "npm ci --only=production",
    "deploy": "npm run test && npm run build && pm2 reload ecosystem.config.js"
  }
}

롤백 전략

# Git 태그로 버전 관리
git tag v1.0.0
git push origin v1.0.0

# 배포
git checkout v1.0.0
npm ci --only=production
pm2 reload my-app

# 롤백
git checkout v0.9.9
npm ci --only=production
pm2 reload my-app

블루-그린 배포

# Nginx 설정
upstream backend {
    server localhost:3000;  # Blue (현재)
}

# 배포 시:
# 1. Green 환경에 새 버전 배포 (포트 3001)
# 2. 테스트
# 3. Nginx 설정 변경
upstream backend {
    server localhost:3001;  # Green (새 버전)
}
# 4. Nginx 리로드
# 5. Blue 환경 종료

정리

핵심 요약

  1. PM2: 프로세스 관리, 클러스터 모드, 자동 재시작
  2. Docker: 컨테이너화, 환경 일관성
  3. Nginx: 리버스 프록시, SSL, 로드 밸런싱
  4. AWS: EC2, Elastic Beanstalk, Lambda
  5. CI/CD: GitHub Actions, 자동 배포
  6. 모니터링: 로그, 메트릭, 헬스체크

배포 방식 비교

방식장점단점사용 사례
VPS + PM2완전한 제어관리 부담중소규모
Docker환경 일관성학습 곡선마이크로서비스
PaaS간편함제한적빠른 프로토타입
서버리스자동 확장Cold Start이벤트 기반

배포 체크리스트

코드:

  • 테스트 통과
  • Linter 통과
  • 프로덕션 빌드
  • 의존성 최신화

설정:

  • 환경 변수 설정
  • 데이터베이스 마이그레이션
  • SSL 인증서
  • 방화벽 규칙

모니터링:

  • 로깅 설정
  • 에러 추적
  • 성능 모니터링
  • 알림 설정

보안:

  • 비밀 키 관리
  • HTTPS 적용
  • Rate Limiting
  • 보안 헤더

다음 단계

  • Node.js 성능 최적화
  • Node.js 보안 심화
  • Node.js 마이크로서비스

추천 학습 자료

도구:

플랫폼:


관련 글

  • Node.js 시작하기 | 설치, 설정, Hello World
  • Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드