서버에 Node.js 앱 배포하는 전체 과정
로컬에서 개발한 앱을 실제 서버에 올려서 누구나 접속할 수 있게 만드는 과정이 배포입니다. 처음 해보면 막막하게 느껴지지만 전체 흐름을 알고 나면 그렇게 복잡하지 않습니다. 서버를 만들고, 환경을 세팅하고, 코드를 올리고, 실행하고, 외부에서 접속할 수 있게 연결하는 것이 전부입니다. 이번 글에서는 클라우드 서버에 Node.js 앱을 배포하는 전체 과정을 처음부터 끝까지 순서대로 정리해보겠습니다. 이전 글들에서 다뤘던 개념들이 실제로 어떻게 연결되는지 확인할 수 있을 겁니다.
전체 흐름 미리 보기
배포 과정을 큰 단계로 나누면 이렇습니다. 서버를 생성하고, SSH로 접속해서 기본 환경을 세팅하고, 코드를 서버에 올리고, 앱을 실행하고, Nginx로 외부 요청을 연결하고, 도메인과 SSL을 적용합니다. 하나씩 순서대로 진행하겠습니다.
1단계 — 서버 생성
클라우드 서비스에서 서버를 하나 만듭니다. AWS EC2, Vultr, DigitalOcean, Oracle Cloud 등 어디서든 상관없습니다. 이 글에서는 일반적인 클라우드 서버를 기준으로 설명합니다.
서버를 만들 때 선택해야 하는 것들이 있습니다. 운영체제는 Ubuntu 22.04 LTS를 선택합니다. LTS는 장기 지원 버전이라 안정적이고 관련 자료도 가장 많습니다. 사양은 소규모 프로젝트라면 CPU 1코어, 메모리 1GB 정도면 충분합니다. 트래픽이 늘어나면 나중에 업그레이드하면 됩니다.
서버를 만들면 공인 IP 주소가 하나 할당됩니다. 이 IP로 서버에 접속합니다. AWS EC2를 사용한다면 탄력적 IP를 할당받아서 고정 IP를 확보해야 합니다. 서버를 재시작해도 IP가 바뀌지 않게 하기 위해서입니다.
보안 그룹이나 방화벽 설정에서 22번 포트(SSH), 80번 포트(HTTP), 443번 포트(HTTPS)를 열어둡니다. 22번은 서버에 접속하기 위해, 80번과 443번은 웹 트래픽을 받기 위해 필요합니다.
2단계 — SSH로 서버 접속
서버가 만들어지면 SSH로 접속합니다. 서버 생성 시 다운받은 키 파일을 사용합니다.
chmod 600 my-key.pem
ssh -i my-key.pem ubuntu@서버IP주소
이전 글에서 다뤘던 것처럼 키 파일의 권한이 600이 아니면 SSH 접속이 거부됩니다.
접속이 되면 먼저 시스템 패키지를 업데이트합니다.
sudo apt update
sudo apt upgrade -y
새 서버를 받으면 패키지가 오래된 상태일 수 있기 때문에 가장 먼저 업데이트를 해주는 게 좋습니다.
3단계 — Node.js 설치
Ubuntu에 기본으로 설치된 Node.js는 버전이 낮은 경우가 많습니다. 최신 LTS 버전을 설치하려면 NodeSource 저장소를 추가하는 게 편합니다.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
설치가 끝나면 버전을 확인합니다.
node -v
npm -v
Node.js 20 버전대가 나오면 정상입니다. npm도 함께 설치됩니다.
여러 프로젝트에서 다른 Node.js 버전을 사용해야 한다면 nvm(Node Version Manager)을 설치하는 방법도 있습니다. 하지만 서버 하나에 앱 하나만 돌린다면 위 방법이 더 간단합니다.
4단계 — 프로젝트 코드 가져오기
로컬에서 개발한 코드를 서버로 가져와야 합니다. 가장 일반적인 방법은 Git을 사용하는 겁니다.
서버에 Git이 설치되어 있는지 확인합니다. Ubuntu에는 보통 기본 설치되어 있습니다.
git --version
프로젝트를 배포할 디렉토리를 만들고 코드를 클론합니다.
sudo mkdir -p /var/www
cd /var/www
sudo git clone https://github.com/사용자명/프로젝트명.git today-play
비공개 저장소라면 SSH 키를 서버에 등록하거나 Personal Access Token을 사용해야 합니다.
코드를 가져왔으면 디렉토리 소유자를 현재 사용자로 변경합니다.
sudo chown -R ubuntu:ubuntu /var/www/today-play
이전 글에서 다뤘던 chown 명령어입니다. 소유자를 바꿔두지 않으면 나중에 파일 수정이나 npm install에서 권한 문제가 생길 수 있습니다.
프로젝트 디렉토리로 이동해서 의존성을 설치합니다.
cd /var/www/today-play
npm install
5단계 — 환경 변수 설정
이전 글에서 다뤘던 것처럼 .env 파일은 저장소에 올리지 않기 때문에 서버에서 직접 만들어야 합니다.
nano .env
프로젝트에서 필요한 환경 변수를 입력합니다.
NODE_ENV=production
PORT=3000
DB_HOST=localhost
DB_PORT=3306
DB_USER=myuser
DB_PASSWORD=실제비밀번호
DB_NAME=myapp
저장한 뒤 파일 권한을 설정합니다.
chmod 600 .env
앱을 간단히 실행해서 에러 없이 돌아가는지 확인합니다.
node app.js
정상적으로 실행되면 Ctrl+C로 종료합니다. 여기까지는 테스트 실행이고, 실제 운영에서는 이렇게 직접 실행하면 안 됩니다. 터미널을 닫으면 앱도 같이 꺼지기 때문입니다.
6단계 — PM2로 앱을 안정적으로 실행
서버에서 Node.js 앱을 운영할 때는 PM2라는 프로세스 매니저를 사용합니다. PM2는 앱을 백그라운드에서 실행하고, 앱이 에러로 종료되면 자동으로 재시작해주고, 서버가 재부팅되어도 앱이 자동으로 다시 뜨게 해줍니다.
PM2를 전역으로 설치합니다.
sudo npm install -g pm2
앱을 PM2로 실행합니다.
cd /var/www/today-play
pm2 start app.js --name today-play
실행 중인 앱 목록을 확인합니다.
pm2 list
상태가 online으로 나오면 정상입니다.
로그를 확인하고 싶으면 이렇게 합니다.
pm2 logs today-play
서버가 재부팅됐을 때 PM2가 자동으로 앱을 시작하게 설정합니다.
pm2 startup
pm2 save
pm2 startup을 실행하면 시스템 시작 시 PM2를 자동 실행하는 명령어가 출력됩니다. 출력된 명령어를 복사해서 실행하면 됩니다. pm2 save는 현재 실행 중인 앱 목록을 저장해서 재부팅 후에 복원할 수 있게 합니다.
앱을 재시작하거나 중지하는 명령어도 알아두면 좋습니다.
pm2 restart today-play
pm2 stop today-play
pm2 delete today-play
7단계 — Nginx 설정
지금 상태에서는 앱이 3000번 포트에서 돌아가고 있습니다. 브라우저에서 서버IP:3000으로 접속하면 앱이 보입니다. 하지만 실제 서비스에서는 사용자가 포트 번호를 입력하지 않습니다. 도메인만 입력하면 접속되어야 합니다.
이전 글에서 다뤘던 Nginx를 리버스 프록시로 사용해서 80번 포트로 들어오는 요청을 3000번 포트의 앱으로 전달합니다.
Nginx를 설치합니다.
sudo apt install nginx
사이트 설정 파일을 만듭니다.
sudo nano /etc/nginx/sites-available/today-play
다음 내용을 입력합니다.
server {
listen 80;
server_name today-play.com www.today-play.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_cache_bypass $http_upgrade;
}
}
proxy_pass가 핵심입니다. 80번 포트로 들어온 요청을 localhost:3000으로 넘기는 설정입니다. proxy_set_header들은 원래 요청의 정보를 앱에 전달하기 위한 헤더 설정입니다. X-Real-IP는 실제 클라이언트의 IP를 전달하는데, 이게 없으면 앱에서 모든 요청의 IP가 127.0.0.1로 보입니다.
Upgrade와 Connection 헤더는 WebSocket 연결을 지원하기 위한 설정입니다. 이전 글에서 다뤘던 WebSocket을 사용하는 앱이라면 이 헤더가 반드시 있어야 합니다.
설정 파일을 활성화하고 기본 설정을 비활성화합니다.
sudo ln -s /etc/nginx/sites-available/today-play /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
설정 문법을 확인하고 Nginx를 재시작합니다.
sudo nginx -t
sudo systemctl restart nginx
이제 브라우저에서 서버 IP 주소만 입력하면 포트 번호 없이 앱에 접속됩니다.
8단계 — 도메인 연결
이전 글에서 다뤘던 도메인 연결을 진행합니다. DNS 관리 페이지에서 A레코드를 추가합니다.
타입: A 호스트: @ 값: 서버IP주소
타입: CNAME 호스트: www 값: today-play.com
DNS 전파까지 시간이 좀 걸릴 수 있지만 보통 몇 분이면 됩니다. nslookup으로 확인합니다.
nslookup today-play.com
서버 IP가 나오면 연결이 완료된 겁니다.
9단계 — SSL 인증서 적용
이전 글에서 다뤘던 Let’s Encrypt로 SSL 인증서를 적용합니다.
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d today-play.com -d www.today-play.com
이메일을 입력하고 약관에 동의하면 인증서가 발급되고 Nginx 설정이 자동으로 업데이트됩니다. HTTP 요청을 HTTPS로 리다이렉트할지 물어보면 리다이렉트를 선택합니다.
브라우저에서 https://todayfunplay.com으로 접속해서 자물쇠 아이콘이 뜨는지 확인합니다.
자동 갱신도 확인합니다.
sudo certbot renew --dry-run
에러가 없으면 90일마다 자동으로 갱신됩니다.
10단계 — 기본 보안 설정
배포가 끝났으면 보안 설정을 해야 합니다. 이전 글에서 다뤘던 서버 보안 기본 설정을 적용합니다.
방화벽을 설정합니다.
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
SSH, HTTP, HTTPS만 허용하고 나머지 포트는 전부 차단합니다. 앱이 사용하는 3000번 포트는 외부에서 직접 접근할 필요가 없습니다. Nginx가 내부에서 연결해주기 때문입니다.
SSH 포트 변경이나 root 로그인 비활성화 같은 추가 보안 설정도 이전 글을 참고해서 적용하면 좋습니다.
배포 후 업데이트 과정
코드를 수정하고 다시 배포할 때는 이 과정을 반복합니다.
cd /var/www/today-play
git pull origin main
npm install
pm2 restart today-play
코드를 최신으로 받고, 새로 추가된 패키지가 있으면 설치하고, 앱을 재시작합니다. 이 네 줄이면 업데이트가 끝납니다.
이 과정이 반복되면 이전 글에서 다뤘던 GitHub Actions로 자동화할 수 있습니다. main 브랜치에 푸시하면 서버에 SSH로 접속해서 위 명령어들을 자동으로 실행하는 워크플로우를 만들면 됩니다.
문제가 생겼을 때 확인하는 순서
배포 후에 사이트가 안 열릴 때 당황하지 않으려면 확인 순서를 알아두는 게 좋습니다.
먼저 앱이 돌아가고 있는지 확인합니다.
pm2 list
pm2 logs today-play
앱 상태가 online이 아니거나 로그에 에러가 있으면 앱 자체의 문제입니다. 환경 변수가 빠져 있거나 의존성 설치가 안 됐거나 코드에 에러가 있을 수 있습니다.
앱이 정상이면 Nginx를 확인합니다.
sudo nginx -t
sudo systemctl status nginx
설정 문법에 에러가 없고 Nginx가 active 상태인지 확인합니다.
Nginx도 정상이면 방화벽을 확인합니다.
sudo ufw status
80번과 443번 포트가 열려 있는지 확인합니다.
방화벽도 정상이면 DNS를 확인합니다.
nslookup today-play.com
도메인이 올바른 IP를 가리키고 있는지 확인합니다.
이 순서대로 안쪽부터 바깥쪽으로 확인해나가면 대부분의 문제를 찾을 수 있습니다.
마무리
서버 배포의 전체 과정을 정리하면 서버 생성, 환경 세팅, 코드 배포, PM2로 실행, Nginx 리버스 프록시, 도메인 연결, SSL 적용, 보안 설정입니다. 한 번에 보면 단계가 많아 보이지만 각 단계는 이전 글들에서 다뤘던 내용의 조합입니다. 처음 한 번은 시간이 걸리지만 두세 번 해보면 흐름이 손에 익고, 그 이후에는 자동화할 부분이 보이기 시작합니다. 다음 글에서는 서버가 느려졌을 때 원인을 찾는 방법을 다뤄보겠습니다.
Leave a Reply