Jenkins와 Docker를 이용한 Next.js 프로젝트 배포
어느 정도 개발 짬바가 쌓이면(그래봤자 3년인데 ㅎㅋ) 처음부터 내가 만들고 배포를 하는 과정은 생각보다 쉽습니다. 가장 어려운 건 이미 모든 게 구축된 상황에서 내가 중간에 투입돼서 추가 개발할 때, 레거시 코드를 개선해야 할 때, 기존 프로세스를 그대로 유지하고 무언가를 해야 할 때 등등.... 내가 중간에 하게 되었을 때가 가장 힘들고 어렵습니다.
특히나 아직 주니어이기 때문에 제가 잘 모르는 분야인데, 심지어 내가 알고 있는 3~4가지 방법이 안된다면 정말 멘붕입니다. 바로 이번 배포 과정이 그랬습니다. 저는 지금까지 제 개인 프로젝트를 배포한 경험만 있었고, 늘 해당 원격 서버에 프로젝트를 clone해서 pm2로 프로젝트를 무중단 상태로 만들었습니다. 거기에 최근은 github actions를 이용해 자동 배포를 경험해 봤습니다. 세세하게는 nginx로 프록시 설정을 해줬고요. 물론 이 과정도 당시에는 헤매면서 배웠지만 지금 돌이켜 생각해 보면 꽤나 쉬웠던 환경이라고 생각합니다. 왜냐면 그냥 안되면 갈아엎고 다시 설정하면 됐고, 정말 작고 쉬운 규모라서 기본 틀이 되는 코드 그대로 작성해 주면 앵간해선 돌아갔습니다.
하지만 기존 운영되고 있는 프로젝트가 있는 원격 서버에 추가 배포를 해야한다면? 진짜 혹시라도 잘 못 건들까 봐 식은땀 줄줄 흘리며 배포했습니다...ㅋㅋㅋ 그 시행착오 과정을 적어보려고 합니다.
배포 환경
사실 처음 경험해보는 환경이라서 아직도 제대로 파악은 못했습니다. 아마 제가 말하는 개념이라든가 단어가 다를 수도 있고... 아무튼! 이해한 대로 설명을 하자면, 현재 원격 서버 환경은 이렇습니다.
vultr(서버 호스팅) + docker(+dokcer-compose) + jenkins + nginx(+ssl, https 적용됨)
이런 환경에서 실제 서비스중이고 유저 유입이 있는 프로젝트 3개가 굴러가고 있던 환경이었습니다.
사실은 그냥 제 편한 방법대로 배포해도 상관은 없었으나, Jenkins를 한 번도 써본 적이 없어서 이 참에 공부 겸 구축해봐야겠다 싶었고, 보통 실무는 이런 식으로 되어있기에 그 프로세스를 그대로 따라가야 해서 저도 똑같은 방법으로 구축해 보게 되었습니다
Dockerfile 작성
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]
EXPOSE 5000
위와 같이 프로젝트 root에 Dockerfile을 작성해줬습니다
작업 디렉터리에 package.json을 복사하고 패키지를 다운로드 한 다음 모두 복사해 줍니다. 그리고 Next.js를 빌드하고 빌드된 파일을 실행시킵니다. port도 설정해 줬습니다.
Jenkins 설정
Jenkins는 이미 구축이 되어있는 상태이니 저는 새로운 아이템을 추가해 줬습니다. 해당 Jenkins 주소로 들어가서 새로운 아이템 클릭 후
이미지와 같이 freestyle project를 클릭해 줬습니다
근데 다 구축하고 느낀 건 파이프라인으로 시작하는 게 더 좋을 수도 있겠다란 생각도 들었습니다. 파이프라인은 freestyle에서 포맷에 맞게 선택하여 설정해 준걸 그냥 스크립트로 직접 작성해 준 거랑 같은 개념이더라고요. 파이프라인으로 직접 해보진 않아서 잘 모르겠으나 freestyle에선 개념이 모호해서 헷갈렸던 부분이 파이프 라인에선 직접 작성해도 되니까 편할 것 같고, 더 확장성도 있지 않을까? 란 생각도 들었습니다. 아무튼 전 몰랐고(ㅋㅋㅋ) freestyle project로 시작!
github project 클릭하고 해당 프로젝트 github 주소를 넣어줬습니다
소스 코드 관리에서 git을 선택합니다
여기서 git 주소를 넣을 때 ssh 주소로 넣어줘야 합니다. 전 사실 이 부분에서부터 이미 헤맸고요...ㅋㅋㅋㅋ 그냥 https주소로 넣으면 될까나? 했는데 그러면 안 됩니다 ssh 주소로 넣어줬고, Credentials 이 부분에 ssh 액세스 키를 클릭해줘야 합니다. 아예 처음 설정 하시는 분들은 별도로 설정을 해주셔야 할 겁니다. 저는 이미 ssh 키가 있길래 기존 거 클릭해 줬습니다.
그리고 어떤 브랜치의 코드를 가져올 건지 설정하는데 master가 되는 브랜치를 선택해 주시면 됩니다. 저는 그게 main이어서 main으로 설정!
그다음 빌드 유발, 빌드 환경이 있는데 이 부분은 건너뛰었습니다. 사실 여기서 추가 설정이 필요할 것 같은데 그건 아래에서 다시 한번 언급하겠습니다
가장 많이 헤맸던 빌드 단계입니다
일단 별도로 설정하지 않는 이상 이 docker 빌드 과정들을 execute shell로 설정하면 docker 명령어를 인식하지 못해서 실패로 뜨더라고요. 사실 개인 플젝, 아무것도 없는 환경이면 원격서버에서 jenkins 설정에 docker 명령어를 인식할 수 있게 바꿔줬겠지만, 이미 있던 플젝들도 그렇게 안되어있고... 그런 거 보면 그냥 나도 건들지 말자 싶어서 execute docker command를 추가해 줬습니다.
차례대로 docker 이미지를 빌드, 생성합니다. 맨 처음에 이 과정에서 기존 플젝이랑 충돌 나면 어떻게 하지 하고 겁먹었는데 $WORKSPACE 이 부분이 해당 젠킨스의 네임스페이스더라고요 그래서 격리? 분리? 되기 때문에 다른 플젝과 충돌할 일은 없었습니다
그리고 원하시는 대로 설정해주시면 됩니다 $BUILD_NUMBER 이 부분은 버전을 뜻합니다. 그리고 tag image를 달아주는데 이건 다른 플젝도 이렇게 하시길래... 관리 포인트인가 싶어서 똑같이 설정해줬습니다. 이렇게 설정해 주니 이미지가 생성될 때마다 latest도 업데이트되는 건지? 새 이미지랑 종속이 되더라고요. 그래서 최신 버전 이미지 삭제하면 같이 삭제됨...ㅋㅋㅋㅋㅋㅋ 아무튼... 백업용으로 그런 설정을 하신 걸 수도 있으니...? 그 룰을 따라갔습니다
그리고 기존에 있는 docker 컨테이너를 중지시키고 삭제합니다.
사실 이 과정은 매우 나쁩니다^^ 이렇게 됨으로써 서비스 정지 잠깐 발생하거든요. 그래서 다른 플젝 보면 docker-compose로 무중단 배포 환경을 만드신 것 같은데 제가 docker를 잘 아는 것도 아니고 지금 사실 이 과정도 단번에 잘 풀렸던 게 아니라서 일단 잘 되면 그다음에 적용하자는 마음으로 작업을 미뤄뒀습니다 하하. 어차피 저는 백오피스라서 잠깐 정지돼도 괜찮거든요(?) 아무튼 그래서 기존 컨테이너를 삭제해 줍니다
여기에서 좀 헤맸는데요. 이제 빌드된 도커 이미지로 새로운 컨테이너를 띄워줘야 합니다. 어떤 이미지를 쓸 것인지와 환경변수, 노출될 포트, 포트 바인딩이 중요합니다.
- Image name : 최신 이미지 이름으로 설정
- Environment variables : 프로젝트에 쓰이는 env 환경변수를 여기다가 써주시면 됩니다. 줄 바꿈 해주면 알아서 잘 인식해 주더라고요
- Exposed ports : 이건 Next.js를 시작할 때 쓰는 포트랑 맞춰야 합니다. 우리가 npm run start 했을 때 localhost:???? 라고 포트가 있잖아요? 그 번호로 써주셔야 합니다
- Port bindings : 저는 5000:3000으로 했는데 이 뜻은, 유저가 외부에서 http://(주소):5000으로 들어오면 우리 프로젝트의 3000으로 서빙한다라고 생각하시면 됩니다
그리고 생성한 컨테이너를 실행시켜 주면 됩니다
저는 이 과정도 만만치 않게 오래 걸렸는데요.. execute shell로 설정해줬다가 계속 빌드 에러 떠서 헤맸고, 환경변수 어디서 설정해야 하는 거지? 싶어서 헤맸습니다...ㅋㅋㅋㅋㅋㅋ 저랑 환경이 비슷하다면 이대로 설정했을 때 아무 문제없을 겁니다.
그리고 build now를 시작해 주시면 됩니다. 그럼 알아서 github에서 플젝 구성을 끌고 오더라고요. 저는 이다음 뭐 해줘야 하나 싶어서 헤맸는데 버튼 클릭 한방이면 끝남^^ㅋㅋㅋㅋㅋㅋㅋ 오히려 생각이 너무 많아서 산으로 가는 건가 싶고요... 아무튼 그렇습니다. 아쉬운 건 별도의 설정을 안 해줘서 main 브랜치에 push 하고 이렇게 매번 build now를 눌러줘야 한다는 겁니다. 이것도 어떻게 수정해야 할지 찾아봐야 할 것 같아요
nginx 설정
가장 많이 헤맸던 nginx 설정 부분입니다
일단 기존에 쓰는 포트들이랑 겹치면 안 되니까 사용 중인 포트를 확인했습니다
$ sudo netstat -tuln
LISTEN이라고 써져 있으면 사용중인 포트입니다. 여기서 5000번을 안 쓰시길래 5000번으로 선택했습니다.
server {
listen 5000;
listen [::]:5000;
server_name 원격서버주소;
location / {
proxy_pass http://원격서버주소:5000;
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;
}
}
그리고 아무것도 몰랐던 저는 일단 기본틀인 코드를 긁어와 위와 같이 작성해 줬습니다
그리고 nginx -t 명령어를 쳐서 문제가 없는지 확인하려는데 nginx 명령어가 먹히질 않더라고요. 여기서부터 멘붕이 시작됩니다. 내가 겪어왔던 환경이랑 다른데...? nginx 프로세스를 확인하기 위해 ps aux | grep nginx 명령어를 쳐줬습니다
이렇게만 보니 잘 모르겠어서...ㅋㅋㅋ nginx: master process nginx -g daemon off 문구를 검색해 보니,
nginx의 데몬 모드가 비활성화된 상태로 실행되고 있음을 의미. 일반적으로 데몬 모드로 실행되는데, 여기에 -g daemon off 옵션이 있다는 건 nginx가 포그라운드에서 실행된다는 걸 의미한다. 이건 주로 디버깅 목적이나, docker 컨테이너에서 사용.
이 개념을 보니 현재 원격 서버에서 실행 중인 docker 컨테이너 중에 nginx가 있다는 게 생각나더라고요. 저는 이게 왜 있었나 싶었는데 이 개념과 연관이 있는 것 같습니다.
근데 깨달아도...ㅋㅋㅋ 뭐 어쩌지? 싶더라고요. 그래서 맨 처음에 시도한 건 nginx 컨테이너에 들어가서 nginx.conf를 바꿔줘 보자였습니다. 똑같이 위와 같이 설정해줬고, nginx -t 명령어를 실행해 보니 먹히더라고요. 그다음 nginx 재시작을 해줬지만...
아무런 변화도 없었습니다^^!!!!!!!!
이대로 포기해야 하나
정말 더 이상의 방법도 생각 안 나고, 오류도 없고, 반응도 없으니 멘붕이었습니다.
그러다가 방화벽 문제인 걸까? 싶었는데 아니었습니다. 왜냐면 별도로 설정해 준 게 아무것도 없더라고요. 한마디로 그냥 모두에게 열려있는 상태라고 생각하면 됩니다.
그러다가, 다른 분께 조언을 구했고 여러 가지를 시도해 봤습니다
server {
listen 80;
listen [::]:80;
server_name 원격서버주소;
location / {
proxy_pass http://원격서버주소:5000;
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;
}
}
다른 플젝의 nginx 코드를 보니 다들 80 포트를 받고 있더라고요?? 그래서 저희도 80으로 해주고, 뭐 더 수정했는데 같이 해서 기억이 안났...(서브 도메인을 다르게 설정했던가) 지금 이 코드 문제없는 코드 같긴 한데 아무튼... 뭔가를 더 수정했다 치고!
여기서 포인트가!!!!! docker를 재시작해줘야 했던 거였습니다!!!!!!
$ sudo systemctl restart docker
물론 해당 서비스가 멈춥니다..ㅎㅎㅎ 근데 트래픽이 많은 서비스가 아니라서 그냥 과감히 하라고 하셔서 했습니다. 저는 nginx 컨테이너 안에서 nginx 재시작을 해줬으니 변화가 있을 거라고 생각했던 건데 아니었나 봅니다. 또한, 도커가 재시작됨에 따라 알아서 변경된 nginx 설정 파일을 읽어 들이는 건지 뭔지... 이런 상황은 첨이라 검색을 어떻게 해야 할지도 모르겠네요ㅋㅋㅋㅋㅋ그냥 우리가 Next.js 설정파일 수정하면 플젝 재시작해야 되는 거랑 같은 개념인 건지... 아무튼!!!!
그랬더니 드디어!!!!!! 변화가 생겼습니다!!!!! 물론 오류였지만욬ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
다른 서비스에 에러가 파파가파ㅏ파파랖ㄱ 뜨길래 바로 롤백해줬습니다. 그래도 에러라도 어딘가요ㅠㅠ 무반응보단 나아... 포트 문제였나, 서브도메인 문제였나? 충돌이 일어나는 것 같길래 쨌든 문제없게끔 최종적으로 nginx 코드를 더 다듬었더니 드디어!!! 해당 주소에 접근할 수 있게 되었습니다
Next.js 에러
Only plain objects
드디어 제대로 배포가 된 걸까 싶었는데, 해당 URL에 들어가면 API 통해서 받아와야 하는 데이터가 안 불러와지는 이슈가 있었습니다. 정확한 에러 파악을 위해 로그를 확인했습니다
$ docker logs (컨테이너 ID)
확인 결과 이런 에러가 뜨더라고요
Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.
그대로 구글링 해보니 server 사이드에 실행되는 api 데이터를 받아올 때 평범한 오브젝트여야 한다는 말이 있더라고요.
https://stackoverflow.com/questions/77091418/warning-only-plain-objects-can-be-passed-to-client-components-from-server-compo
이렇게 해줬으나 실패
디버깅이 정확히 안되길래 결국 docker 이미지 빌드할 때 npm start가 아니라 npm run dev로 해놓고 컨테이너에 직접 들어가 코드 쳐가면서 로그를 확인해 보니 환경변수가 없어서였습니다.........ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
네....... 위에서는 제가 설정하라고 설명했죠? 저거 저는 뒤늦게 설정해 줬습니다....ㅋㅋㅋㅋㅋㅋ 삽질 한번 했고요.....
Error occurred prerendering page
이것도 마주했던 에러인데, react-quill 라이브러리를 쓰고 있었는데 이 에디터가 랜더링이 안되더라고요.
해당 에디터를 import 하는 부분을 서버 측 렌더링 비활성화로 바꿔주니까 해결되었습니다
Port 에러
이건 제가 Next.js port랑 맞춰야되는지 모르고 5000:5000으로 설정해 문제가 발생했습니다. 꼭 잊지말기...
(외부에서 접속해줬으면 하는 port):(Next.js port) 이렇게 맞춰줍시다!!!!
마무리
진짜 너무너무 힘든 과정이었습니다. 무려 이틀정도가 걸렸습니다. 정리하고 보니까 그냥 제가 너무 놓친 건가 싶었던 것 같기도 한데, 저에겐 매우 조심해야 하는 상황인데 또 경험해보지 못했던 상황들이고, jenkins도 처음 써보는 거라 많이 헤맸던 것 같습니다. 그래도 잘 배포되어서 너무 행복하네요ㅠㅠㅠ
한 가지 nginx에서 server_name을 지정해 줬는데 적용이 안되더라고요. 이럴 땐 DNS 설정을 해줘야 하는데, 저에겐 이건 권한이 없어서...ㅋㅋㅋ다른 분이 대신해주셨습니다. 에효 설정해야 할 게 엄청 많네요
또한, Next.js가 업그레이드됨에 따라 배포 되었을때 잡히는 에러가 따로 발생하더라고요. 위 에러중 라이브러리 랜더링 이슈는 개발 모드에선 발견할 수 없었던 이슈입니다. Next.js도 점점 규약이 늘면서 신경써야할 부분들이 많아졌네요
아무튼 jenkins 처음 써봤고!!! 하나 더 알아갈 수 있어서 좋네요! 사실 전에 혼자 해보려다가 실패한 경험이 있었거든요. 지금 와서 생각해보면 제가 설정을 덜 해줬던 것 같은데 그 당시에는 무반응이라서 그냥 포기했었습니다ㅋㅋㅋㅋ 이젠 어떻게 해야할지 알게되었으니까 여기저기 써먹을 수 있겠어요. 하나 더 남은 숙제인 main 브랜치에 push 했을때 자동으로 build되게 하는것만 작업하면 되겠네요.
이젠 점점 프론트엔드가 아니라 풀스택에 가까워지는 기분이네요. 물론 시간이 지남에 따른 당연한 결과이지만 ㅎㅎ