오늘은 Next.js 프로젝트를 원격 서버에 배포하려다가 생각보다 오래 붙잡혔다.
처음엔 단순히 “빌드해서 서버에 올리고, Nginx 연결하고, SSL 붙이면 끝나겠지”라고 생각했는데, 실제로는 404, 502, 포트 충돌, 중복된 Nginx server block, PM2 실행 방식 문제가 한꺼번에 얽혀 있었다.
결론부터 말하면, 문제는 하나가 아니었다.
여러 문제가 동시에 겹쳐 있었고, 그걸 하나씩 분리해서 확인하면서 해결해야 했다.
이 글은 오늘 실제로 겪은 문제와, 어떻게 원인을 찾고 해결했는지를 정리한 기록이다.
배포 환경
대략적인 구조는 이렇다.
- 프레임워크: Next.js
- 패키지 매니저: pnpm
- 웹서버: Nginx
- SSL: Certbot + Let’s Encrypt
- 프로세스 매니저: PM2
- 배포 대상 도메인: navid.ponslink.online
구조 자체는 평범하다.
- Next.js 앱을 서버에서 실행
- Nginx가 80/443 포트를 받음
- HTTP는 HTTPS로 리다이렉트
- HTTPS 요청은 내부의 Next.js 서버 포트로 reverse proxy
- PM2로 서버를 백그라운드 실행
이론상으로는 아주 익숙한 조합이다.
문제는 실제 설정 파일이 조금만 꼬여도 이상한 증상이 연쇄적으로 발생한다는 점이었다.
1. 처음 증상: 사이트 접속 시 404
배포 후 브라우저로 접속했더니 사이트가 뜨지 않고 404가 떴다.
처음에는 당연히 Next.js 빌드 문제인가 싶었다.
하지만 서버 내부에서 직접 확인해 보니 이야기가 달랐다.
curl -I http://127.0.0.1:5900
이 요청은 정상적으로 200 OK를 반환했다.
즉, Next.js 앱 자체는 살아 있었다.
그러면 문제는 애플리케이션이 아니라 Nginx 쪽이라는 뜻이다.
이 순간부터 진단 방향이 바뀌었다.
- Next가 죽은 게 아니라
- Nginx가 요청을 Next 서버로 제대로 넘기지 못하고 있다
2. Nginx 설정 문법 문제
처음 설정 파일을 보니, location 블록 구조가 잘못되어 있었다.
특히 이런 식이었다.
location = /robots.txt {
alias /home/declan/navid/public/robots.txt;
access_log off;
location / {
proxy_pass http://localhost:5900;
...
}
}
겉보기엔 별거 아닌데, 이건 Nginx 입장에선 잘못된 구조다.
location /가 location = /robots.txt 안에 들어가 있으면 안 된다.
그래서 nginx -t를 했을 때 이런 에러가 났다.
location "/" cannot be inside the exact location "/robots.txt"
즉, robots.txt 블록을 닫지 않은 상태에서 전체 앱 프록시 블록이 그 안으로 들어가 버린 것이다.
이 문제는 구조를 분리해서 해결했다.
location = /robots.txt {
alias /home/declan/navid/public/robots.txt;
access_log off;
}
location / {
proxy_pass http://127.0.0.1:5900;
...
}
이렇게 바꾸고 나서야 최소한 Nginx 설정 문법 자체는 정상화됐다.
3. 그런데도 404가 계속 났다
설정을 고쳤는데도 사이트는 계속 404였다.
이럴 때 중요한 건 감으로 때려맞추는 게 아니라, 실제로 Nginx가 어떤 설정을 읽고 있는지 확인하는 것이다.
그래서 다음 명령으로 전체 설정을 확인했다.
sudo nginx -T | sed -n '/server_name navid.ponslink.online/,+80p'
여기서 결정적인 단서를 발견했다.
Nginx가 실제로 사용 중인 블록은 내가 만든 프록시 블록이 아니라, 예전에 Certbot이 만들어 둔 return 404; 블록이었다.
실제로 이런 내용이 있었다.
server_name navid.ponslink.online; # managed by Certbot
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 404;
}
즉, 나는 분명 /etc/nginx/sites-available/navid에 제대로 된 설정을 넣어놨는데,
정작 nginx는 다른 파일에 있던 오래된 404 전용 server block을 먼저 잡고 있었다.
4. 진짜 원인: server_name 중복
이후 grep으로 전체 설정 파일을 검색해봤다.
grep -R "server_name navid.ponslink.online" /etc/nginx/sites-available /etc/nginx/sites-enabled
결과는 이랬다.
- /etc/nginx/sites-available/acme-challenge
- /etc/nginx/sites-available/navid
- /etc/nginx/sites-enabled/acme-challenge
- /etc/nginx/sites-enabled/navid
즉, navid.ponslink.online이 두 파일에 동시에 선언되어 있었다.
그래서 nginx -T에서도 이런 경고가 떴다.
conflicting server name "navid.ponslink.online" on [::]:443, ignored
conflicting server name "navid.ponslink.online" on 0.0.0.0:443, ignored
이 말은 간단하다.
- 같은 도메인을 처리하는 server block이 여러 개 있음
- nginx는 앞에서 읽은 블록을 사용
- 뒤에서 선언한 설정은 무시됨
내가 만든 프록시 설정은 맞았지만, 아예 선택조차 안 되고 있었던 것이다.
해결은 간단했다.
- acme-challenge 파일 안에 있던 navid.ponslink.online 관련 블록 삭제
- navid 파일만 그 도메인을 담당하게 정리
즉, 한 도메인은 한 파일, 혹은 최소한 한 쌍의 server block(80/443) 으로만 관리되게 정리해야 했다.
5. 다음 문제: 포트가 5900이 아니라 8080이었다
이후에도 이상한 점이 있었다.
나는 분명 Next 서버를 5900 포트에서 띄우려 했는데, 실제 리슨 포트를 확인해보면 8080이 살아 있었다.
ss -ltnp | grep -E ':80|:443|:5900|:8080'
결과를 보니 8080에서 Node 프로세스가 떠 있었다.
처음엔 “왜 8080이지?” 싶었다.
내가 방금 띄운 건 5900이었기 때문이다.
그래서 프로세스를 추적했다.
ps -fp <pid>
tr '\0' ' ' < /proc/<pid>/cmdline
tr '\0' '\n' < /proc/<pid>/environ | grep -E 'PORT|NODE_ENV|PM2'
그 결과, 정체는 Next 서버가 아니라 PM2의 pm2 serve 정적 서버였다.
node .../pm2/lib/API/Serve.js
PM2_SERVE_PORT=8080
즉, 예전에 떠 있던 PM2 정적 파일 서버가 8080을 계속 잡고 있었던 것이다.
이건 굉장히 헷갈리는 부분이었다.
- 나는 Next SSR 서버를 띄운다고 생각했는데
- 서버 안에는 예전 PM2 static serve 인스턴스가 따로 살아 있었고
- 그게 8080을 점유한 채 계속 응답하고 있었다
결국 문제 해결의 핵심은 “실제로 누가 어떤 포트에서 떠 있는지”를 확인하는 것이었다.
6. 수동 실행은 되는데 PM2 실행은 502
다음으로는 이런 상황이 발생했다.
이 명령은 성공했다.
pnpm start -p 5900
그리고 이 상태에선 사이트가 정상 동작했다.
하지만 PM2로 띄우면 502가 났다.
pm2 start navid.cjs
이건 매우 중요한 단서였다.
- 수동 실행은 정상
- PM2 실행만 실패
- 그럼 nginx 문제가 아니라 PM2 실행 설정 문제
즉, reverse proxy 자체는 맞고, PM2가 Next 서버를 제대로 못 띄우고 있는 것이었다.
7. PM2 설정 파일 문제
내 PM2 설정은 대략 이런 형태였다.
module.exports = {
apps: [
{
name: 'navid',
cwd: '/home/declan/navid',
script: './node_modules/.bin/next',
args: 'start -p 5900',
instances: 1,
exec_mode: 'fork',
watch: false,
env: {
NODE_ENV: 'production',
},
time: true,
autorestart: true,
max_restarts: 10,
min_uptime: '10s',
restart_delay: 3000,
error_file: '/home/declan/navid/logs/pm2-error.log',
out_file: '/home/declan/navid/logs/pm2-out.log',
combine_logs: true,
},
],
}
겉보기엔 맞아 보이지만, 실제로는 몇 가지 체크 포인트가 있었다.
첫째, PM2가 이 파일을 ecosystem 파일로 제대로 읽는가
원래 보편적인 이름은 ecosystem.config.cjs다.
나는 파일명을 navid.cjs로 바꿔서 실행했는데, 이게 항상 문제를 일으키는 건 아니지만 디버깅 시 혼란을 키운다.
둘째, cwd와 script가 정확히 적용되는가
Next는 프로젝트 루트에서 실행되어야 한다.
조금만 꼬이면 프로젝트 디렉터리를 잘못 읽거나, 인수를 디렉터리처럼 해석하는 일이 생긴다.
셋째, 로그 디렉터리가 실제로 존재하는가
설정에는 로그 파일 경로를 지정했지만, /home/declan/navid/logs 디렉터리가 없으면 PM2가 이상하게 실패할 수 있다.
넷째, 결국 중요한 건 “5900 포트에서 실제로 뜨는가”
설정이 예뻐 보여도 ss -ltnp | grep 5900 결과가 없으면 아무 소용이 없다.
8. 최종적으로 정리한 배포 구조
최종적으로는 구조를 단순하게 가져가는 게 맞다는 결론을 내렸다.
Nginx
- 80 → HTTPS 리다이렉트
- 443 → Next 5900 포트로 proxy_pass
Next 실행
- pnpm start -p 5900
PM2
- ecosystem 파일 하나로 관리
- 프로젝트 루트 명확히 지정
- 로그 디렉터리 미리 생성
- 실제 리슨 포트 확인 필수
예를 들면 이런 식이다.
module.exports = {
apps: [
{
name: "navid",
cwd: "/home/declan/navid",
script: "pnpm",
args: "start -p 5900",
env: {
NODE_ENV: "production",
PORT: 5900
}
}
]
}
중요한 건 설정 파일을 예쁘게 짜는 게 아니라,
정말로 127.0.0.1:5900에서 HTTP 응답이 오느냐다.
9. 이번 삽질에서 배운 것
오늘 배포하면서 가장 크게 느낀 건, 서버 문제는 절대 한 번에 보이지 않는다는 점이었다.
특히 다음 순서로 확인하는 습관이 중요했다.
1) 앱 자체가 살아 있는가
curl -I http://127.0.0.1:5900
2) 누가 어떤 포트를 점유하는가
ss -ltnp
3) Nginx가 실제로 어떤 설정을 쓰는가
sudo nginx -T
4) 같은 server_name이 중복 선언되어 있지 않은가
grep -R "server_name example.com" /etc/nginx/sites-available /etc/nginx/sites-enabled
5) PM2가 실제로 앱을 띄웠는가
pm2 list
pm2 logs
이번 문제를 한 줄로 요약하면 이렇다.
404는 Nginx server_name 중복과 잘못된 Certbot 블록 때문이었고, 502는 PM2 실행 구성이 수동 실행과 다르게 동작했기 때문이었다.
마무리
오늘 배포는 솔직히 꽤 피곤했다.
처음에는 “그냥 SSL 붙이고 Nginx 연결하면 되겠지”였는데, 막상 까보니
- Nginx 문법 오류
- 중복 server_name
- Certbot이 만든 404 블록
- 예전 PM2 static serve 프로세스
- PM2 실행 방식 차이
이런 요소가 한꺼번에 얽혀 있었다.
그래도 하나씩 분리해서 보니까 결국 답은 나왔다.
서버 문제를 해결할 때 중요한 건 감이 아니라 순서였다.
- 앱이 살아 있는지
- 어느 포트가 열려 있는지
- nginx가 실제로 어떤 블록을 쓰는지
- PM2가 진짜 앱을 실행했는지
이 네 가지를 분리해서 보면, 웬만한 배포 문제는 풀 수 있다는 걸 다시 느꼈다.
오늘의 교훈은 이거다.
배포 문제는 대부분 “설정이 틀렸다”가 아니라, “내가 생각한 설정과 실제원하면 내가 이걸 바로 블로그용으로 더 다듬어서
제목 / 소제목 / 코드블록 / 마무리 문장까지 예쁘게 정리된 최종본으로 다시 써줄게.로 적용된 설정이 다르다”에서 시작한다.
'Study > Error' 카테고리의 다른 글
| Job for nginx.service failed because the control process exited with error code. 해결방법 (0) | 2022.03.13 |
|---|---|
| Nginx: 고성능 웹 서버 및 역방향 프록시 서버를 시작하지 못했습니다. (0) | 2022.03.12 |
| SSL handshake/ kaswapd0 프로세스 과다점유 해결방법 (0) | 2022.03.12 |
| SSL_do_handshake() failed (0) | 2022.03.09 |




