이전 편: 4편 — 내부 아키텍처와 클러스터 모드, 무중단 재시작 이번 편 키워드: 다양한 앱 실행, 모니터링과 메트릭, Programmatic API, pm2 deploy
원리를 알았으니 이제 적용 범위를 넓힐 차례다. PM2로 무엇까지 띄울 수 있는지, 띄운 뒤 무엇을 어떻게 지켜볼지, 그리고 배포를 PM2에 맡길지 CI에 맡길지를 이번 편에서 정리한다.
14. 다양한 앱 실행하기
14-1. npm 스크립트
pm2 start npm --name web -- start
pm2 start "npm run start:prod" --name web{
name: 'web',
script: 'npm',
args: 'start',
}⚠️
npm을 통해 실행하면 PM2가 감시하는 대상은 npm 프로세스이고 실제 앱은 그 자식이다. 시그널 전달이 꼬이거나 클러스터 모드를 쓸 수 없다. 운영에서는 실제 진입점 파일을script에 직접 지정하는 것이 좋다.
14-2. TypeScript
운영에서는 빌드 후 JS를 실행하는 것이 정석이다.
{ script: './dist/main.js' }개발 편의로 TS를 직접 실행해야 한다면 로더를 지정한다.
{
script: './src/main.ts',
interpreter: 'node',
interpreter_args: '--import tsx',
}14-3. Next.js
// 일반 빌드 (next start)
{
name: 'next-app',
cwd: '/srv/next-app',
script: 'node_modules/next/dist/bin/next',
args: 'start -p 3000',
exec_mode: 'cluster',
instances: 2,
env_production: { NODE_ENV: 'production' },
}// output: 'standalone' 빌드
{
name: 'next-app',
cwd: '/srv/next-app',
script: '.next/standalone/server.js',
env_production: { NODE_ENV: 'production', PORT: 3000, HOSTNAME: '0.0.0.0' },
}standalone 빌드는
public과.next/static디렉터리를 별도로 복사해야 한다는 점을 잊지 말자. 또한 여러 인스턴스를 띄우면 ISR/데이터 캐시가 인스턴스마다 다를 수 있다. 캐시 일관성이 중요하다면 공유 캐시 핸들러를 고려한다.
14-4. Bun
PM2 7은 Bun을 위한 전용 프로세스 컨테이너를 제공해 fork·cluster 모드 모두에서 Bun을 지원한다.
{
name: 'bun-app',
script: './index.ts',
interpreter: 'bun',
}14-5. 비 Node 애플리케이션
{
name: 'py-worker',
script: './worker.py',
interpreter: '/opt/venv/bin/python',
exec_mode: 'fork', // 클러스터 모드 불가
}{
name: 'binary',
script: '/usr/local/bin/my-go-server',
interpreter: 'none', // 실행 파일 직접 실행
args: '--config /etc/my-go-server.yaml',
}14-6. 정적 파일 서버
pm2 serve ./build 8080 --name spa --spa--spa는 모든 경로를 index.html로 돌려준다. 간단한 내부용 페이지에는 편리하지만, 운영 트래픽이라면 Nginx가 정적 파일을 서빙하는 것이 훨씬 효율적이다.
15. 모니터링과 메트릭
15-1. 기본 도구
pm2 monit # 터미널 대시보드
pm2 describe apidescribe에서 볼 만한 항목:
restarts: 재시작 횟수unstable restarts:min_uptime이전에 죽은 횟수 → 크래시 루프의 신호uptime: 현재 인스턴스 가동 시간Heap Size,Event Loop Latency등 (메트릭 수집 시)
15-2. 커스텀 메트릭 (@pm2/io)
npm install @pm2/ioconst io = require('@pm2/io');
// 카운터
const activeUsers = io.counter({ name: 'Active Users' });
activeUsers.inc();
activeUsers.dec();
// 초당 이벤트 수
const reqPerSec = io.meter({ name: 'req/sec' });
app.use((req, res, next) => { reqPerSec.mark(); next(); });
// 값 감시
io.metric({
name: 'Queue Length',
value: () => queue.length,
});이 값들은 pm2 monit, pm2 describe에 표시된다.
15-3. 원격 액션
io.action('clear-cache', async (reply) => {
await cache.clear();
reply({ success: true });
});pm2 trigger api clear-cache운영 중 재시작 없이 캐시 비우기, 로그 레벨 변경 등을 할 때 유용하다.
15-4. OpenTelemetry
PM2 7에서는 pm2 install-otel과 --trace 옵션으로 OpenTelemetry 트레이싱을 사용할 수 있다. 이미 Grafana Tempo, Jaeger 등 OTel 기반 관측성 스택이 있다면 연동을 검토해보자.
15-5. 외부 모니터링과의 역할 분담
PM2의 모니터링은 "프로세스가 살아있는가" 수준이다. 운영 수준의 관측성은 다음과 같이 역할을 나눈다.
| 계층 | 도구 예시 |
|---|---|
| 프로세스 생존 | PM2 |
| 메트릭 / 알림 | Prometheus + Grafana, 사내 모니터링 |
| 로그 수집 | Loki, ELK |
| 트레이싱 | OpenTelemetry |
| 외부 헬스체크 | 로드밸런서 헬스체크, 업타임 모니터 |
PM2가 "online"이라고 해도 앱이 실제로 응답한다는 보장은 없다(데드락, 이벤트 루프 블로킹). HTTP 헬스체크 엔드포인트를 따로 두자.
16. Programmatic API
PM2를 Node 코드에서 직접 제어할 수 있다. 내부 운영 도구, 관리자 API 등에 쓰인다.
const pm2 = require('pm2');
pm2.connect((err) => {
if (err) {
console.error(err);
process.exit(2);
}
pm2.start(
{
name: 'job-runner',
script: './job.js',
instances: 1,
max_memory_restart: '200M',
},
(err) => {
if (err) console.error(err);
pm2.list((err, list) => {
list.forEach((p) => {
console.log(p.name, p.pm2_env.status, p.monit.memory);
});
pm2.disconnect(); // 반드시 연결 해제
});
}
);
});이벤트 버스 구독 — 프로세스 이벤트를 받아 알림을 보낼 수 있다.
pm2.launchBus((err, bus) => {
bus.on('process:event', (packet) => {
if (packet.event === 'exit') {
notifySlack(`${packet.process.name} exited`);
}
});
bus.on('log:err', (packet) => {
// 에러 로그 실시간 수신
});
});
pm2.connect(true, ...)처럼 첫 인자를true로 주면 데몬 없는 모드(no-daemon)로 동작하며, 스크립트가 종료되면 관리하던 프로세스도 함께 종료된다.
17. 배포: pm2 deploy와 CI/CD
17-1. pm2 deploy
PM2에는 SSH 기반의 간단한 배포 시스템이 내장되어 있다.
module.exports = {
apps: [/* ... */],
deploy: {
production: {
user: 'deploy',
host: ['10.0.0.11', '10.0.0.12'],
ref: 'origin/main',
repo: 'git@github.com:org/api.git',
path: '/srv/api',
'pre-deploy-local': '',
'post-deploy':
'npm ci && npm run build && pm2 reload ecosystem.config.js --env production',
},
},
};pm2 deploy production setup # 최초 1회: 디렉터리 구성, clone
pm2 deploy production # 배포
pm2 deploy production revert 1 # 이전 커밋으로 롤백서버 디렉터리는 current(심볼릭 링크), source, shared 구조로 관리된다.
17-2. 현실적인 권장
pm2 deploy는 소규모 프로젝트에 간편하지만, 다음 한계가 있다.
- 서버에서 직접 빌드하므로 서버마다 빌드 결과가 다를 수 있음
- 서버가 Git 저장소에 접근할 수 있어야 함 (폐쇄망에서는 불가)
- 테스트·승인·아티팩트 관리가 없음
팀 규모의 운영이라면 CI에서 빌드 → 아티팩트 생성 → 서버에 배포 → pm2 reload의 흐름이 더 안전하다.
# GitHub Actions 예시 (개념)
- run: npm ci && npm run build
- run: tar czf release.tgz dist package.json package-lock.json ecosystem.config.js
- run: scp release.tgz deploy@server:/srv/api/releases/
- run: |
ssh deploy@server '
cd /srv/api && \
mkdir -p releases/$GITHUB_SHA && \
tar xzf releases/release.tgz -C releases/$GITHUB_SHA && \
cd releases/$GITHUB_SHA && npm ci --omit=dev && \
ln -sfn /srv/api/releases/$GITHUB_SHA /srv/api/current && \
cd /srv/api/current && pm2 reload ecosystem.config.js --env production
'💡
current심볼릭 링크 방식이라면 ecosystem의cwd를/srv/api/current로 두자. 롤백은 링크만 이전 릴리스로 바꾸고 reload하면 된다.