본문으로 건너뛰기 Node.js 배포 가이드 | PM2, Docker, AWS, Nginx

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

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

이 글의 핵심

Node.js 배포 가이드: PM2, Docker, AWS, Nginx. PM2 (Process Manager)·Docker.

들어가며

배포 체크리스트

로컬에서 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

  • 보안 헤더

다음 단계

추천 학습 자료

도구


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Node.js 배포 가이드: PM2, Docker, AWS, Nginx. PM2 (Process Manager)·Docker로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Node.js, 배포, PM2, Docker, AWS, Nginx, DevOps 등으로 검색하시면 이 글이 도움이 됩니다.