sudo와 콘솔 복구 경로, 고유한 <APP_USER>·<APP_SLUG>·내부 listen port
NODE RUNTIME
Node.js LTS·nvm·PM2 운영 구성
Ubuntu 전용 서비스 계정에 검증된 nvm과 Node.js v24 LTS를 고정하고 PM2 ecosystem·systemd startup·로그·재시작 정책을 백업과 롤백이 가능한 순서로 구성합니다.BEFORE YOU START
시작 전에 준비하세요
검증된 애플리케이션 artifact와 package lock, 정상 revision 및 smoke test
Node v24 LTS의 승인된 정확한 patch와 PM2 고정 버전·checksum을 기록한 변경 승인
비밀은 ecosystem 파일이 아닌 권한 제한 파일 또는 조직 secret manager로 전달하는 설계
권장 대상 Node.js 애플리케이션의 런타임 버전, 재부팅 자동 시작, 장애 재시작과 안전한 되돌리기를 직접 운영하는 담당자
FOLLOW THE RECIPE
8단계 구성·점검 레시피
지원 LTS·계정·자원 확인
Node 공식 릴리스 표에서 v24가 LTS인지 재확인하고 서비스 계정·CPU·메모리·현재 런타임을 읽기 전용으로 조사합니다. Current나 EOL major를 운영 기본값으로 선택하지 않습니다.
cat /etc/os-release
uname -mgetent passwd <APP_USER>
id <APP_USER>sudo -iu <APP_USER> bash -lc 'if [ -s "$HOME/.nvm/nvm.sh" ]; then . "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION> >/dev/null 2>&1 || true; fi; command -v node || true; node --version 2>/dev/null || true; command -v pm2 || true; pm2 --version 2>/dev/null || true'nproc
free -h
df -hT / /home /srv- Node v24 LTS의 현재 지원 상태와 승인 patch를 기록했습니다.
- 서비스는 root가 아닌 전용 계정으로 실행합니다.
- 애플리케이션 최대 메모리와 로그·artifact 여유 공간을 확인했습니다.
기존 system Node와 nvm Node가 섞여 있거나 계정이 없다면 설치 전에 소유권과 실행 경로를 설계합니다.
nvm·Node·PM2·기존 startup unit 충돌을 확인합니다.
기존 nvm·PM2·포트·프로세스 점검
같은 서비스 계정의 shell 초기화 파일, PM2 home, systemd unit, listen port를 확인해 중복 daemon과 path drift를 찾습니다. 환경 변수 전체나 PM2 jlist는 비밀을 노출할 수 있어 출력하지 않습니다.
sudo -iu <APP_USER> bash -lc 'test -d "$HOME/.nvm/.git" && git -C "$HOME/.nvm" status --short --branch || true'sudo -iu <APP_USER> bash -lc 'if [ -s "$HOME/.nvm/nvm.sh" ]; then . "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION> >/dev/null 2>&1 || true; fi; pm2 list 2>/dev/null || true; pm2 ping 2>/dev/null || true'systemctl list-unit-files 'pm2-*' --no-pager
systemctl status pm2-<APP_USER>.service --no-pager -lsudo ss -lntp | grep -E ':<APP_PORT>\b'- PM2를 실행할 Unix 계정과 PM2_HOME이 하나로 고정됐습니다.
- 동일 포트·동일 애플리케이션의 기존 process manager가 없습니다.
- 현재 서비스의 정상 revision과 복구 경로를 기록했습니다.
같은 애플리케이션을 systemd와 PM2가 동시에 관리하면 재시작 루프가 생길 수 있으므로 관리자 하나만 선택합니다.
원격 script pipe 없이 고정된 nvm tag를 설치합니다.
nvm v0.40.6·Node v24 LTS·PM2 고정 버전 설치
nvm upstream Git 저장소의 고정 tag를 서비스 계정으로 clone하고 승인한 Node patch와 PM2 버전을 설치합니다. nvm 환경에서는 npm global 설치에 sudo를 사용하지 않습니다.
sudo -iu <APP_USER> git clone --branch v0.40.6 --depth 1 https://github.com/nvm-sh/nvm.git /home/<APP_USER>/.nvm대상 디렉터리가 비어 있고 tag·commit을 변경 기록에서 검토한 신규 설치에만 실행합니다.sudo -iu <APP_USER> git -C /home/<APP_USER>/.nvm describe --tags --exact-match
sudo -iu <APP_USER> git -C /home/<APP_USER>/.nvm rev-parse HEADsudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm install <APPROVED_NODE_24_VERSION>; nvm alias default <APPROVED_NODE_24_VERSION>; nvm use <APPROVED_NODE_24_VERSION>'sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION>; npm install --global pm2@<APPROVED_PM2_VERSION>'- nvm describe 결과가 v0.40.6이며 commit을 기록했습니다.
- node --version이 승인한 v24 patch입니다.
- PM2는 latest 별칭이 아닌 승인된 정확한 버전입니다.
설치 버전이 다르면 startup을 만들지 말고 shell profile·nvm alias·npm prefix 충돌을 먼저 수정합니다.
shell profile과 PM2 ecosystem 파일을 백업하고 편집합니다.
shell·ecosystem 설정 백업과 편집
서비스 계정의 기존 shell 파일과 ecosystem 파일을 고유한 접미사로 보관하고, 코드·cwd·memory·restart 정책만 명시합니다. API key·DB password 같은 비밀은 이 파일에 쓰지 않습니다.
if sudo test -f /home/<APP_USER>/.bashrc; then sudo cp --archive --no-clobber /home/<APP_USER>/.bashrc /home/<APP_USER>/.bashrc.<BACKUP_SUFFIX>; fi
sudoedit /home/<APP_USER>/.bashrcif sudo test -f /srv/<APP_SLUG>/ecosystem.config.cjs; then sudo cp --archive --no-clobber /srv/<APP_SLUG>/ecosystem.config.cjs /srv/<APP_SLUG>/ecosystem.config.cjs.<BACKUP_SUFFIX>; fi
sudoedit /srv/<APP_SLUG>/ecosystem.config.cjs원본이 존재할 때만 백업하고 서비스 계정이 artifact·설정 파일을 덮어쓸 필요가 없도록 root 소유를 유지합니다.sudo chown root:<APP_USER> /srv/<APP_SLUG>/ecosystem.config.cjs
sudo chmod 0640 /srv/<APP_SLUG>/ecosystem.config.cjs
sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION> >/dev/null; node --check /srv/<APP_SLUG>/ecosystem.config.cjs'export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"서비스 계정의 shell에 한 번만 추가합니다. systemd는 shell profile을 자동으로 읽지 않으므로 startup unit에는 절대 경로가 필요합니다.module.exports = {
apps: [{
name: "<APP_SLUG>",
cwd: "/srv/<APP_SLUG>/current",
script: "./dist/server.js",
instances: 1,
exec_mode: "fork",
watch: false,
min_uptime: "10s",
max_restarts: 5,
exp_backoff_restart_delay: 100,
kill_timeout: 10000,
env: { NODE_ENV: "production", PORT: "<APP_PORT>" }
}]
};실제 secret은 넣지 않습니다. cluster mode는 애플리케이션이 stateless이고 세션·WebSocket·작업 중복을 검증한 경우에만 별도로 설계합니다.- 기존 shell profile·ecosystem 파일은 고유한 suffix로 백업했고, 원본이 없었던 파일은 신규 생성으로 변경 기록에 남겼습니다.
- script·cwd·port가 실제 artifact와 일치합니다.
- watch는 운영에서 꺼져 있고 제한된 restart backoff·max_restarts를 사용합니다.
- ecosystem 파일에 비밀 literal이 없습니다.
문법 오류, 서비스 계정 쓰기 가능 artifact, 무제한 재시작 또는 watch가 있으면 시작하지 않습니다.
foreground 실행으로 애플리케이션 자체를 먼저 검증합니다.
foreground 검증 후 PM2와 systemd startup 등록
같은 계정과 Node 경로로 짧은 foreground smoke test를 수행한 뒤 PM2를 시작합니다. startup unit은 출력된 command를 맹목적으로 붙여넣지 말고 승인된 절대 경로와 계정을 확인합니다.
sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION>; node --version; npm --version; pm2 --version; test -r /srv/<APP_SLUG>/current/dist/server.js'sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION>; pm2 start /srv/<APP_SLUG>/ecosystem.config.cjs --only <APP_SLUG>'sudo env PATH=/home/<APP_USER>/.nvm/versions/node/<APPROVED_NODE_24_VERSION>/bin:/usr/bin:/bin /home/<APP_USER>/.nvm/versions/node/<APPROVED_NODE_24_VERSION>/bin/pm2 startup systemd -u <APP_USER> --hp /home/<APP_USER>경로·계정·home을 실제 값과 대조한 뒤 실행합니다. Node 버전 변경 후에는 공식 PM2 안내대로 startup unit을 다시 생성해야 합니다.sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION> >/dev/null; pm2 save'- PM2 process가 online이고 restart count가 안정적입니다.
- pm2-<APP_USER>.service가 정확한 nvm Node·PM2 절대 경로를 사용합니다.
- 저장한 목록에는 의도한 앱만 있습니다.
errored·재시작 증가·잘못된 PATH가 보이면 save하지 말고 로그와 foreground exit code를 확인합니다.
listen·HTTP·재부팅 복원과 장애 지표를 검증합니다.
프로세스·포트·HTTP·재부팅 복원 검증
PM2 상태만 보지 않고 실제 listen 주소, local health, systemd unit 경로와 재시작 횟수를 확인합니다. 운영 재부팅은 승인된 점검창에서만 수행합니다.
sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION> >/dev/null; pm2 describe <APP_SLUG>; pm2 logs <APP_SLUG> --lines 100 --nostream'로그에는 token·고객 데이터가 포함될 수 있으므로 외부 공유 전 마스킹합니다.sudo ss -lntp | grep -E ':<APP_PORT>\b'
curl -fsS http://127.0.0.1:<APP_PORT>/<HEALTH_PATH>systemctl cat pm2-<APP_USER>.service
systemctl is-enabled pm2-<APP_USER>.service
systemctl is-active pm2-<APP_USER>.servicesudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION> >/dev/null; pm2 list'- 애플리케이션은 의도한 loopback 또는 사설 주소에만 listen합니다.
- health·핵심 smoke test가 성공합니다.
- restart count가 관찰 구간 동안 증가하지 않습니다.
- startup unit의 ExecStart·PATH가 승인 버전을 가리킵니다.
health가 성공해도 오류율·의존 서비스·업무 흐름이 비정상이라면 배포 성공으로 판단하지 않습니다.
계정·비밀·artifact·로그 권한과 재시작 폭주 방지를 확인합니다.
최소 권한·비밀·로그·공급망 점검
PM2와 Node를 root로 실행하지 않고 application code는 서비스 계정이 수정할 수 없게 합니다. ecosystem·로그·dump에 비밀이나 개인정보가 남지 않도록 권한과 보존 정책을 확인합니다.
stat -Lc '%a %U:%G %n' /srv/<APP_SLUG> /srv/<APP_SLUG>/ecosystem.config.cjs /srv/<APP_SLUG>/current/dist/server.jssudo -iu <APP_USER> bash -lc 'find "$HOME/.pm2" -maxdepth 2 -type f -printf "%m %u:%g %p\n" | head -100'sudo ss -lntp | grep -E ':<APP_PORT>\b'
sudo ufw status verbosesudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION> >/dev/null; npm --global ls --depth=0; npm audit --omit=dev --package-lock-only --prefix /srv/<APP_SLUG>/current'- 서비스 계정은 login·sudo 권한을 최소화했고 code·ecosystem은 root 소유입니다.
- 앱 port는 인터넷에 직접 노출하지 않고 승인된 reverse proxy만 접근합니다.
- 비밀은 코드·ecosystem·shell history·PM2 로그에 없습니다.
- dependency lock과 취약점 검토 결과를 배포 기록에 남겼습니다.
- 재시작 backoff·max_restarts와 log rotation·용량 알림이 있습니다.
- <...> 자리표시자는 승인된 리터럴 값으로 직접 치환하고 외부 입력으로 shell 명령을 조립하거나 eval하지 않습니다.
공개 listen, 서비스 계정 소유 code, 평문 비밀 또는 무제한 로그가 있으면 운영 전환을 중단합니다.
이전 Node·PM2·artifact로 되돌릴 절차를 확인합니다.
이전 artifact·Node·PM2 startup으로 복구
복구도 서비스 중단을 일으킬 수 있습니다. 이전 artifact와 Node·PM2 호환성, process dump, startup unit 경로를 확인하고 승인된 버전으로 한 번만 되돌립니다.
sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <APPROVED_NODE_24_VERSION> >/dev/null; pm2 list; pm2 describe <APP_SLUG>'
systemctl cat pm2-<APP_USER>.servicesudo ln -sfn /srv/<APP_SLUG>/releases/<APPROVED_PREVIOUS_RELEASE> /srv/<APP_SLUG>/current
sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <PREVIOUS_NODE_VERSION>; pm2 startOrReload /srv/<APP_SLUG>/ecosystem.config.cjs --only <APP_SLUG>'DB migration·queue schema와 이전 코드 호환성을 확인한 뒤 실행합니다.sudo env PATH=/home/<APP_USER>/.nvm/versions/node/<PREVIOUS_NODE_VERSION>/bin:/usr/bin:/bin /home/<APP_USER>/.nvm/versions/node/<PREVIOUS_NODE_VERSION>/bin/pm2 startup systemd -u <APP_USER> --hp /home/<APP_USER>
sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <PREVIOUS_NODE_VERSION> >/dev/null; pm2 save'systemctl is-active pm2-<APP_USER>.service
curl -fsS http://127.0.0.1:<APP_PORT>/<HEALTH_PATH>
sudo -iu <APP_USER> bash -lc '. "$HOME/.nvm/nvm.sh"; nvm use <PREVIOUS_NODE_VERSION> >/dev/null; pm2 list'if sudo test -f /home/<APP_USER>/.bashrc.<BACKUP_SUFFIX>; then sudo cp --archive /home/<APP_USER>/.bashrc.<BACKUP_SUFFIX> /home/<APP_USER>/.bashrc; fi
if sudo test -f /srv/<APP_SLUG>/ecosystem.config.cjs.<BACKUP_SUFFIX>; then sudo cp --archive /srv/<APP_SLUG>/ecosystem.config.cjs.<BACKUP_SUFFIX> /srv/<APP_SLUG>/ecosystem.config.cjs; fi해당 backup이 실제 변경 직전 파일인지 checksum과 suffix를 확인한 뒤 복원합니다.sudo systemctl disable --now pm2-<APP_USER>.service
sudo install -d -o root -g root -m 0700 <APPROVED_QUARANTINE_DIR>
if sudo test -f /srv/<APP_SLUG>/ecosystem.config.cjs; then sudo mv --no-clobber /srv/<APP_SLUG>/ecosystem.config.cjs <APPROVED_QUARANTINE_DIR>/ecosystem.config.cjs.new; fi
if sudo test -L /etc/systemd/system/multi-user.target.wants/pm2-<APP_USER>.service; then sudo mv --no-clobber /etc/systemd/system/multi-user.target.wants/pm2-<APP_USER>.service <APPROVED_QUARANTINE_DIR>/pm2-<APP_USER>.service.link; fi기존 backup이 없고 change record가 이 레시피의 신규 생성 파일임을 증명할 때만 사용합니다. bashrc는 전체를 이동하지 말고 기록된 nvm 추가 줄만 sudoedit로 제거합니다. Node·PM2 package와 application data는 purge하지 않습니다.- 이전 release·Node·PM2 경로가 모두 같은 승인 묶음입니다.
- health와 핵심 업무가 회복됐고 restart count가 안정적입니다.
- 신규 nvm·Node 디렉터리는 즉시 삭제하지 않고 사후 분석까지 보존합니다.
코드만 되돌리고 schema·queue message가 호환되지 않으면 장애가 커질 수 있습니다. 데이터 담당자의 복구 계획을 우선합니다.
원인과 실제 exit code·버전·rollback 시간을 기록하고 staging에 동일한 startup 검사를 추가합니다.
SECURITY CHECK
운영 전 마지막 보안 점검
- Node는 공식 LTS의 승인 patch, nvm·PM2는 고정 tag·version을 사용합니다.
- remote installer를 shell pipe로 실행하지 않고 nvm upstream Git tag를 검토합니다.
- 서비스 계정은 root가 아니며 배포 artifact와 ecosystem 파일을 수정할 수 없습니다.
- 비밀은 ecosystem·shell history·PM2 dump·로그에 평문으로 넣지 않습니다.
- 애플리케이션 port는 loopback·사설망에만 열고 TLS reverse proxy 뒤에 둡니다.
- 재시작 폭주와 로그 고갈을 막는 max_restarts·backoff·rotation·알림을 둡니다.
- <...> 자리표시자는 승인된 리터럴 값으로 직접 치환하고 외부 입력으로 shell 명령을 조립하거나 eval하지 않습니다.
COMMON ERRORS
자주 막히는 지점
nvm: command not found
- 증상
- SSH shell에서는 되지만 systemd·PM2 startup에서 nvm과 node를 찾지 못합니다.
- 가능한 원인
- nvm은 shell 함수이고 systemd는 사용자 shell profile을 자동으로 읽지 않으며 Node 경로가 버전별 디렉터리입니다.
- 확인 순서
- unit의 PATH·ExecStart를 승인 Node 절대 경로로 재생성하고 Node 변경 때 startup hook도 갱신합니다.
PM2 앱이 계속 errored
- 증상
- online 직후 exit하고 restart count가 계속 증가합니다.
- 가능한 원인
- 잘못된 cwd·script·환경 또는 의존 서비스 실패와 너무 공격적인 autorestart 정책일 수 있습니다.
- 확인 순서
- save·반복 restart를 멈추고 foreground exit code, 제한된 로그, ecosystem 경로를 확인합니다.
재부팅 뒤 앱이 다른 Node로 실행
- 증상
- 수동 실행 버전과 boot 복원 버전이 다릅니다.
- 가능한 원인
- nvm default alias만 바꾸고 PM2 systemd unit의 절대 PATH를 다시 만들지 않았을 수 있습니다.
- 확인 순서
- systemctl cat으로 실제 경로를 확인하고 승인 Node 버전으로 startup unit을 재생성합니다.
PRIMARY REFERENCES
공식 문서
- Node.js · 공식 릴리스와 LTS 상태
- nvm-sh · nvm 공식 설치와 사용법
- PM2 · Quick Start와 startup hook
- PM2 · Ecosystem File 옵션
설치 저장소와 지원 버전은 바뀔 수 있습니다. 검토일 이후에는 링크된 공식 문서와 현재 서버의 패키지 후보 버전을 함께 확인하세요.