PM2 + Nginx 리버스 프록시: Node.js 앱을 80번 포트에 연결

Ubuntu 24.04 서버에 Node.js PM2 Nginx 리버스 프록시 구성을 올려, 앱 실행은 PM2가 담당하고 80번 포트로 들어온 요청은 Nginx가 앱으로 넘기게 설정했습니다. 앱 프로세스를 강제로 종료하거나 서버를 재부팅해도 PM2가 앱을 다시 실행하는 것까지 확인했습니다.
아래 결과 상자는 일반 사용자 deploy로 실행한 출력입니다. 도메인 자리에는 예시 값 example.com을 두고, 서버 안에서 Host 헤더를 붙여 요청했습니다.
1️⃣ Node.js 설치
Node.js는 NodeSource 저장소를 apt에 등록해 설치하며, 버전은 2026년 11월 기준 LTS인 Node.js 26으로 정했습니다.
🔲 NodeSource setup_26.x
export DEBIAN_FRONTEND=noninteractive이 값을 먼저 두면 패키지 설치 중 설정을 묻는 창이 뜨지 않습니다. 아래 sudo 명령의 -E 옵션은 이 값을 root 권한으로 실행하는 명령에도 그대로 넘깁니다.
curl -fsSL https://deb.nodesource.com/setup_26.x | sudo -E bash -2026-11-07 16:08:16 - Repository configured successfully.
2026-11-07 16:08:16 - To install Node.js, run: apt install nodejs -y스크립트가 저장소 등록을 마치면 Node.js를 Ubuntu 패키지처럼 apt로 설치할 수 있습니다.
🔲 apt-get install nodejs
sudo -E apt-get install -y nodejsSetting up libatomic1:amd64 (14.2.0-4ubuntu2~24.04.1) ...
Setting up nodejs (26.13.0-1nodesource1) ...
Processing triggers for libc-bin (2.39-0ubuntu8.9) ...
Processing triggers for man-db (2.12.0-4build2) ...🔲 node -v, npm -v
node -vv26.13.0npm -v11.20.0npm은 nodejs 패키지에 함께 들어 있어 따로 설치하지 않습니다.
2️⃣ Node.js 시험 앱
PM2와 Nginx가 제대로 연결되는지 보기 위해, 요청을 받으면 글자 한 줄을 반환하는 시험 앱을 만듭니다.
🔲 app.js
mkdir -p ~/hello홈 폴더 아래 hello 폴더를 만들고, 그 안에 app.js 파일을 작성합니다.
cat > ~/hello/app.js <<'APP'
const http = require('http');
http.createServer((req, res) => {
console.log(req.method, req.url);
res.end('hello from node\n');
}).listen(3000, '127.0.0.1');
APP앱은 127.0.0.1의 3000번 포트에서만 요청을 받으므로 서버 바깥에서는 바로 열리지 않습니다. 바깥 요청은 Nginx가 받아 이 포트로 넘기게 설정합니다.
3️⃣ PM2 앱 실행과 자동 시작
PM2는 Node.js 앱을 백그라운드에서 실행하고, 앱 프로세스가 종료되면 다시 실행하는 프로세스 관리 도구입니다. 서버가 부팅될 때 앱을 다시 올리는 자동 시작까지 이 장에서 설정합니다.
🔲 npm install -g pm2
sudo npm install -g pm2added 45 packages in 5spm2 -v[PM2] Spawning PM2 daemon with pm2_home=/home/deploy/.pm2
[PM2] PM2 Successfully daemonized
7.1.2pm2 명령을 처음 실행하면 PM2 데몬이 함께 시작되며, 설치된 PM2는 7.1.2 버전입니다.
🔲 pm2 start
cd ~/helloapp.js가 있는 폴더로 옮긴 뒤 PM2로 앱을 실행합니다.
pm2 start app.js --name hello[PM2] Starting /home/deploy/hello/app.js in fork_mode (1 instance)
[PM2] Done.
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17257 │ 0s │ 0 │ online │ 0% │ 26.3mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘--name은 PM2 목록에 보일 앱 이름이며, 이후 명령은 이 이름으로 앱을 지정합니다.
pm2 list┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17257 │ 2s │ 0 │ online │ 0% │ 58.5mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0% | ram usage: 38.8% | disk: ⇓ 0mb/s ⇑ 0mb/sstatus 칸의 online은 앱이 실행 중이라는 뜻이고, ↺ 칸은 PM2가 앱을 다시 시작한 횟수입니다.
🔲 curl 127.0.0.1:3000
서버 안에서 요청을 보내면, 앱이 반환한 글자 한 줄이 그대로 나옵니다.
curl -sS http://127.0.0.1:3000/hello from node🔲 pm2 restart
앱 코드를 고친 뒤에는 앱을 다시 시작해 반영합니다.
pm2 restart helloUse --update-env to update environment variables
[PM2] Applying action restartProcessId on app [hello](ids: [ 0 ])
[PM2] [hello](0) ✓
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17306 │ 0s │ 1 │ online │ 0% │ 19.3mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0% | ram usage: 38.8% | disk: ⇓ 0mb/s ⇑ 0mb/spm2 list┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17306 │ 2s │ 1 │ online │ 0% │ 58.3mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0% | ram usage: 38.8% | disk: ⇓ 0mb/s ⇑ 0mb/s다시 시작한 뒤 pid가 17257에서 17306으로 바뀌고 ↺ 칸이 1로 올랐습니다.
🔲 kill -9
앱 프로세스가 갑자기 종료되는 상황은 kill -9로 만들 수 있으며, pm2 pid는 앱의 프로세스 번호를 출력합니다.
kill -9 "$(pm2 pid hello)"3초 뒤 PM2 목록을 다시 확인합니다.
pm2 list┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17797 │ 3s │ 2 │ online │ 0% │ 58.6mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 51.2% | ram usage: 35.8% | eth0: ⇓ 1kb/s ⇑ 1kb/s | eth1: ⇓ 19kb/s ⇑ 0mb/s | disk: ⇓ 728kb/s ⇑ 3.1mb/sPM2가 앱을 새 pid 17797로 다시 실행해 online 상태였고, ↺ 칸은 2로 올라 있었습니다.
🔲 pm2 save
pm2 save[PM2] Saving current process list...
[PM2] Successfully saved in /home/deploy/.pm2/dump.pm2pm2 save는 지금 실행 중인 앱 목록을 dump.pm2 파일에 저장하며, 부팅 때 PM2는 이 목록대로 앱을 다시 실행합니다.
🔲 pm2 startup
부팅 때 PM2가 실행되게 하려면 PM2를 systemd 서비스로 등록합니다. 아래 명령은 지금 계정 이름과 홈 폴더를 넣어 서비스를 만듭니다.
sudo env PATH="$PATH" pm2 startup systemd -u "$(id -un)" --hp "$HOME"ExecStart=/usr/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/usr/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/usr/lib/node_modules/pm2/bin/pm2 kill
[Install]
WantedBy=multi-user.target
Target path
/etc/systemd/system/pm2-deploy.service
Command list
[ 'systemctl enable pm2-deploy' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-deploy.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-deploy...
Created symlink /etc/systemd/system/multi-user.target.wants/pm2-deploy.service → /etc/systemd/system/pm2-deploy.service.
[PM2] [v] Command successfully executed.env PATH는 root 권한으로 실행하는 pm2가 지금 계정의 node 경로를 찾게 하는 부분이며, -u와 --hp는 서비스를 실행할 계정과 홈 폴더입니다. 서비스 파일의 ExecStart 줄은 pm2 resurrect이며, 부팅 때 저장해 둔 앱 목록을 되살립니다.
systemctl status pm2-deploy --no-pager | head -n 5○ pm2-deploy.service - PM2 process manager
Loaded: loaded (/etc/systemd/system/pm2-deploy.service; enabled; preset: enabled)
Active: inactive (dead)
Docs: https://pm2.keymetrics.io/enabled는 부팅 때 서비스가 실행된다는 뜻입니다. pm2 startup은 서비스를 등록만 하고 지금 실행하지는 않아 inactive (dead)로 나오며, 서비스는 다음 부팅부터 실행됩니다.
🔲 재부팅 뒤 pm2 list
sudo systemctl reboot2분쯤 뒤 deploy로 다시 접속해 앱 상태를 확인합니다.
uptime -pup 2 minutespm2 list┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 976 │ 108s │ 0 │ online │ 0% │ 63.0mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0.3% | ram usage: 35.1% | disk: ⇓ 4kb/s ⇑ 4kb/s서버가 켜진 지 2분 된 시점에 앱은 이미 108초째 online 상태였으며, PM2가 부팅과 함께 앱을 다시 실행했습니다.
4️⃣ Nginx 리버스 프록시 설정
앱이 127.0.0.1:3000에서만 요청을 받으므로, 80번 포트로 들어온 요청은 Nginx가 받아 앱으로 넘깁니다. 이렇게 앞에서 요청을 받아 뒤의 서버로 전달하는 구성을 리버스 프록시라고 합니다.
🔲 apt-get install nginx
sudo -E apt-get install -y nginxSetting up nginx-common (1.24.0-2ubuntu7.19) ...
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /usr/lib/systemd/system/nginx.service.
Setting up nginx (1.24.0-2ubuntu7.19) ...nginx -vnginx version: nginx/1.24.0 (Ubuntu)Ubuntu 24.04 저장소의 Nginx는 1.24.0이며, 설치와 함께 부팅 때 실행되도록 등록됩니다.
🔲 /etc/nginx/sites-available/hello
/etc/nginx/sites-available/hello 파일을 만들어 아래 내용을 넣습니다.
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}proxy_pass는 요청을 넘길 앱 주소입니다. Host와 X-Real-IP, X-Forwarded-For, X-Forwarded-Proto 헤더는 앱이 원래 요청의 도메인과 방문자 IP, http 또는 https 여부를 알 수 있게 넘기는 값입니다.
proxy_http_version 1.1과 Upgrade, Connection 헤더는 WebSocket 연결을 앱까지 넘길 때 필요합니다. Upgrade와 Connection 헤더는 프록시를 지나면 앱으로 전달되지 않으므로 설정에서 직접 넣습니다.
sudo ln -sf /etc/nginx/sites-available/hello /etc/nginx/sites-enabled/hellosites-enabled 폴더에 링크를 만든 설정만 Nginx가 읽습니다.
🔲 nginx -t
sudo nginx -tnginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successfulsudo systemctl reload nginxnginx -t로 설정 문법을 먼저 검사하고, 통과하면 reload로 Nginx를 멈추지 않고 새 설정을 적용합니다.
🔲 curl Host 헤더 요청
curl -sS -i -H 'Host: example.com' http://127.0.0.1/ | head -n 4HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Sat, 07 Nov 2026 08:09:38 GMT
Content-Length: 16설정을 다시 읽은 뒤 Host 헤더를 넣어 보낸 요청에 앱 응답이 돌아왔습니다. 응답 헤더의 Server 줄은 nginx이고, Content-Length 16은 앱이 반환한 hello from node 한 줄(줄바꿈 포함 16바이트)과 같은 크기입니다. 서버 바깥에서 접속하려면 방화벽에서 80번 포트가 열려 있어야 합니다.
5️⃣ Certbot HTTPS 적용
Certbot의 Nginx 플러그인을 사용하면 인증서 발급과 Nginx 설정 변경을 한 번에 진행할 수 있습니다. 도메인이 서버 IP를 가리키는 상태에서 아래 명령을 실행합니다.
sudo certbot --nginxCertbot 설치와 자동 갱신 테스트는 아래 글에서 확인할 수 있으며, 그 글의 발급 명령은 Apache 기준이라 발급 단계만 위 명령으로 바꿔 실행합니다.
🔢 FAQ & 추천 콘텐츠
ℹ️ 제휴 안내
본 사이트의 콘텐츠에는 제휴 링크가 포함되어 있습니다. 방문자가 이 링크를 통해 상품 또는 서비스를 구매하면 본 사이트는 판매처로부터 수수료를 지급받습니다. 이 과정에서 구매자가 지불하는 금액(이벤트 할인 시 금액이 내려갑니다↓)은 오르지 않습니다. 게시된 가격·할인·재고 정보는 작성 시점 기준이며 실제와 다를 수 있으므로, 구매 전 판매처에서 최종 확인하시기 바랍니다. 상품 선정과 평가는 자체 기준에 따라 작성되며, 수수료 지급 여부가 소개 순서나 평가 내용에 영향을 주지 않습니다.