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

공정위 광고 문구 Ver 3.1
공정위 광고 문구 Ver 3.2(모바일 전용)
테스트 환경Debian 계열Ubuntu 24.04.5 LTS일반 사용자 계정
Node.js 앱 운영과 리버스 프록시 작업 확인

Ubuntu 24.04 서버에 Node.js PM2 Nginx 리버스 프록시 구성을 올려, 앱 실행은 PM2가 담당하고 80번 포트로 들어온 요청은 Nginx가 앱으로 넘기게 설정했습니다. 앱 프로세스를 강제로 종료하거나 서버를 재부팅해도 PM2가 앱을 다시 실행하는 것까지 확인했습니다.

아래 결과 상자는 일반 사용자 deploy로 실행한 출력입니다. 도메인 자리에는 예시 값 example.com을 두고, 서버 안에서 Host 헤더를 붙여 요청했습니다.

🎯 요점
🗂️ 요청 흐름
📂 Nginx리버스 프록시
📄 CertbotHTTPS 인증서
📂 PM2앱 실행과 자동 시작
📄 Node.js 앱
PM2로 앱 실행과 부팅 때 자동 시작
Nginx가 80번 포트 요청을 앱으로 전달
전제 조건
서버초기 설정을 마친 Ubuntu 24.04sudo 권한이 있는 일반 계정도메인은 HTTPS 적용 때 필요

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 nodejs
Setting 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 -v
v26.13.0
npm -v
11.20.0

npm은 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 pm2
added 45 packages in 5s
pm2 -v
[PM2] Spawning PM2 daemon with pm2_home=/home/deploy/.pm2
[PM2] PM2 Successfully daemonized
7.1.2

pm2 명령을 처음 실행하면 PM2 데몬이 함께 시작되며, 설치된 PM2는 7.1.2 버전입니다.

🔲 pm2 start

cd ~/hello

app.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/s

status 칸의 online은 앱이 실행 중이라는 뜻이고, ↺ 칸은 PM2가 앱을 다시 시작한 횟수입니다.

🔲 curl 127.0.0.1:3000

서버 안에서 요청을 보내면, 앱이 반환한 글자 한 줄이 그대로 나옵니다.

curl -sS http://127.0.0.1:3000/
hello from node

🔲 pm2 restart

앱 코드를 고친 뒤에는 앱을 다시 시작해 반영합니다.

pm2 restart hello
Use --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/s
pm2 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/s

PM2가 앱을 새 pid 17797로 다시 실행해 online 상태였고, ↺ 칸은 2로 올라 있었습니다.

🔲 pm2 save

pm2 save
[PM2] Saving current process list...
[PM2] Successfully saved in /home/deploy/.pm2/dump.pm2

pm2 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 reboot

2분쯤 뒤 deploy로 다시 접속해 앱 상태를 확인합니다.

uptime -p
up 2 minutes
pm2 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 nginx
Setting 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 -v
nginx 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/hello

sites-enabled 폴더에 링크를 만든 설정만 Nginx가 읽습니다.

🔲 nginx -t

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx

nginx -t로 설정 문법을 먼저 검사하고, 통과하면 reload로 Nginx를 멈추지 않고 새 설정을 적용합니다.

🔲 curl Host 헤더 요청

curl -sS -i -H 'Host: example.com' http://127.0.0.1/ | head -n 4
HTTP/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번 포트가 열려 있어야 합니다.

🧭 Node.js 앱 요청과 다시 실행
설명 다이어그램
🧭 상황
요청 흐름
🌐브라우저example.com 요청
➜
🧱Nginx80번 포트, proxy_pass
➜
🟢Node.js 앱127.0.0.1:3000
프로세스 관리
⚙️systemdpm2-deploy 서비스
➜
🔁PM2dump.pm2 앱 목록
➜
🟢Node.js 앱hello
브라우저 요청은 80번 포트의 Nginx가 받아 proxy_pass에 적은 127.0.0.1:3000으로 넘기고, 앱의 응답을 그대로 반환합니다.
앱 프로세스가 종료되면 PM2가 같은 앱을 바로 다시 실행합니다. kill -9로 종료하고 3초 뒤 pm2 list를 보니 앱은 새 pid로 online 상태였습니다.
서버가 부팅되면 systemd가 pm2-deploy 서비스를 실행하고, PM2는 pm2 save로 저장한 목록대로 앱을 다시 실행합니다.

5️⃣ Certbot HTTPS 적용

Certbot의 Nginx 플러그인을 사용하면 인증서 발급과 Nginx 설정 변경을 한 번에 진행할 수 있습니다. 도메인이 서버 IP를 가리키는 상태에서 아래 명령을 실행합니다.

sudo certbot --nginx

Certbot 설치와 자동 갱신 테스트는 아래 글에서 확인할 수 있으며, 그 글의 발급 명령은 Apache 기준이라 발급 단계만 위 명령으로 바꿔 실행합니다.

🔢 FAQ & 추천 콘텐츠

Nginx가 proxy_pass에 적은 앱 주소에 연결하지 못하면 502 Bad Gateway를 반환합니다. pm2 stop hello로 앱을 내린 뒤 같은 요청을 보내자 502가 나왔고, /var/log/nginx/error.log에는 [connect() failed (111: Connection refused) while connecting to upstream] 줄과 연결하려던 주소 http://127.0.0.1:3000/이 남았습니다. pm2 list에서 앱이 online인지, 앱이 요청을 받는 포트와 proxy_pass 주소가 같은지부터 확인합니다. 앱을 다시 시작하자 같은 요청이 200으로 돌아왔습니다.

PM2는 앱의 출력을 실행한 계정의 홈 폴더 아래 .pm2/logs 폴더에 앱 이름별 파일로 저장합니다. pm2 logs hello --nostream --lines 5를 실행하자 /home/deploy/.pm2/logs/hello-out.log와 hello-error.log 두 파일의 마지막 줄이 나왔고, out 파일에는 앱이 기록한 GET / 요청이 있었습니다. --nostream 없이 실행하면 새로 쌓이는 로그를 계속 이어서 출력합니다.

자동 시작 서비스는 pm2 unstartup systemd로 지웁니다. 앱을 더하거나 지운 뒤에는 pm2 save를 다시 실행해야 부팅 때 바뀐 목록대로 앱이 실행됩니다. pm2 startup을 실행했을 때도 출력 끝에 이 두 명령이 함께 안내되었습니다.

ℹ️ 제휴 안내
본 사이트의 콘텐츠에는 제휴 링크가 포함되어 있습니다. 방문자가 이 링크를 통해 상품 또는 서비스를 구매하면 본 사이트는 판매처로부터 수수료를 지급받습니다. 이 과정에서 구매자가 지불하는 금액(이벤트 할인 시 금액이 내려갑니다↓)은 오르지 않습니다. 게시된 가격·할인·재고 정보는 작성 시점 기준이며 실제와 다를 수 있으므로, 구매 전 판매처에서 최종 확인하시기 바랍니다. 상품 선정과 평가는 자체 기준에 따라 작성되며, 수수료 지급 여부가 소개 순서나 평가 내용에 영향을 주지 않습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

인간임을 증명하세요: 8   +   7   =