본문 바로가기

nestJS 배포 자동화

@정소민fan2025. 6. 18. 00:08

프론트와 백으로 나누어서 작업하다 보면, 백 쪽은 프론트에게 어떻게 API 명세서를 건네주고, API 서버를 건네줘야 할까?
프론트는 당연히 서버가 있어야 거기에 API를 날려보고 테스트를 진행할 수 있을 것이기 때문에 API 서버를 자신의 로컬에서 실행하든지, 개발 서버를 하나 띄우던지 해야 한다.
프론트 개발자가 직접 자신의 로컬에 API서버를 띄우면 일이 간단하지만, 변경사항이 있을 때 마다 pull을 받아야 하고, 환경설정과 같은 예상치 못한 오류가 나타났을 경우, 백엔드에 대한 지식이 없으면 대처하기가 곤란하다. 개발서버를 두는 것이 (백엔드는 조금 힘들겠지만) 프론트 개발자에게는 좀 더 편할 것이다.

무책임한 백엔드

 
그러면 개발 서버는 어떻게 관리해야 할까? 백엔드 개발자가 쓰는 로컬에서 쓰는 환경과 개발 서버의 환경도 분리해줘야 할 것이고, API에 변경사항이 생기면 이를 즉각 반영해줘야 한다. 변경사항을 반영하는게 좀 고달픈데, EC2에 API 서버를 운영 중이라고 생각해보자.

  1. 변경사항이 생겨서 이를 작업하고 push한다.
  2. 개발 서버에 원격으로 접속한다.
  3. git pull을 수행한다.
  4. 다시 빌드한다.
  5. 서버를 내리고, 다시 시작한다.
  6. 잘 동작하는지 확인한다.

벌써 귀찮다. npm 패키지가 추가되어서 이를 다운받는 일도 비일비재하다. 이를 자동화 해보자.
배포 자동화에는 Jenkins가 유명하지만, 미니 프로젝트에는 거기까지는 필요없을 것 같다.
나는 nestJS를 docker + github action을 이용해서 배포 자동화를 구현했다.
 

Dockerfile 작성

그럼 먼저 nest 프로젝트의 루트 폴더에 Dockerfile을 만들고, 다음을 입력해주자.

# 베이스 이미지
FROM node:20-alpine

# 작업 디렉토리 설정
WORKDIR /app

# 1. package*.json만 복사
COPY package*.json ./

# 2. 캐시 가능한 npm install
RUN npm ci

# 3. 나머지 소스 복사
COPY . .

# 4. NestJS 빌드
RUN npm run build

# 5. 앱 실행
CMD ["node", "dist/main"]


# (선택) 포트 노출
EXPOSE 3000

아마 nestJS를 사용 중이라면 위 내용을 그대로 입력해도 될 것이다.

Dockerhub 

https://www.docker.com/products/docker-hub/ 여기에 들어가서 가입되어있지 않다면 가입을 진행하고 로그인을 해주자.

여기서 dockerhub로 들어가자

우측의 create repository로 들어가자

여기서 원하는 이름을 입력하고 create를 하자.
당신의 아이디와 repository의 이름은 기억해두어야 한다.

github action workflow 작성

사실 이 파트만 해도 따로 포스팅해야할 정도로 큰 볼륨이지만, 여기서는 약식으로 소개하겠다.
하나하나 따라가보자

name: Docker CI/CD

이 workflow의 이름이다

on:
  push:
    branches:
      - main

main 브랜치에 push가 발생했을 때 이 workflow를 실행한다.

jobs:
  build-and-push:
    runs-on: ubuntu-latest

build-and-push라는 이름을 붙인 job을 수행하는데, ubuntu 서버 위에서 동작한다.

    steps:
      - name: Checkout source code
        uses: actions/checkout@v3

이 리포지토리의 소스코드에 체크아웃해서 소스코드에 접근할 수 있도록 한다.

      - name: Cache npm cache
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
          restore-keys: ${{ runner.os }}-node-

npm 라이브러리를 캐싱해두고 사용한다.

      - name: Install dependencies
        run: npm ci

npm 라이브러리를 설치한다. 캐싱되어있다면 캐시에서 가져온다.

      - name: Build project
        run: npm run build

nestJS 빌드를 수행한다.

      - name: Docker login
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

도커에 로그인한다. username과 password에 특이하게 설정이 되어있는데, 이는 github secrets에 담아놓은 인자이다. 후술할테니 넘어가자

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          platforms: linux/arm64
          tags: gooch1744/jujeopton:latest

빌드된 도커 이미지를 푸시한다.
with의 paltform에는 개발 서버의 아키텍처를 쓰도록 하자. 나의 개발 서버는 arm 아키텍처의 ubuntu를 쓰기에 linux/arm64를 사용했다.

      - name: Deploy to SERVER
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_KEY }}
          port: '12000'
          script: |
            docker pull gooch1744/jujeopton:latest
            docker stop jujeopton || true
            docker rm jujeopton || true
            docker run -d --name jujeopton --env-file /{server_path}/env/.env.dev -p 4000:3000 gooch1744/jujeopton:latest

여기서는 서버에 직접 접속하여  docke 이미지를 pull 받은 후에 기존에 켜져있던 API서버를 내리고, 새로 pull 받은 이미지를 컨테이너로 동작시킬 것이다.

  • host : 개발 서버의 ip, 또는 도메인
  • username : 개발 서버에 접속할 계정
  • key : 개발 서버에 접속할 수 있는 private ssh-key, ssh-key가 없다면 이 필드를 password로 바꿔도 된다.
  • port :개발 서버에 ssh로 접속할 수 있는 포트

docker run 명령어가 조금 긴데 일부만 알아보자

  • --name : 컨테이너 이름
  • --env-file : .env 파일로 사용할 파일의 경로, 개발 서버의 경로로 수정하자. 그대로 쓰면 안됨 ㄹㅇ
  • -p 4000:3000 : 컨테이너 내부의 3000번 포트를 개발 서버의 4000번 포트와 연결한다. 따라서 이 개발 서버에 접속하려면 4000포트로 연결하면 된다.
  • 마지막 인자로는 자신의 도커 허브 리포지토리 이름과 태그를 입력하자. 태그는 잘 모르겠으면 latest를 쓰고 더 알고 싶다면 공부하고 오자 

yml 파일 전문은 더보기란에

더보기

여기서는 캐싱까지 구현했으니 그걸 염두에 두고 보자

name: Docker CI/CDAdd commentMore actions

on:
  push:
    branches:
      - main  # main 브랜치에 push 시 실행

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v3

      - name: Cache npm cache
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
          restore-keys: ${{ runner.os }}-node-

      - name: Install dependencies
        run: npm ci

      - name: Build project
        run: npm run build

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Docker login
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Docker cache
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-docker-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-docker-

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          platforms: linux/arm64
          tags: gooch1744/jujeopton:latest
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache

      - name: Deploy to SERVER
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_KEY }}
          port: '12000'
          script: |
            docker pull gooch1744/jujeopton:latest
            docker stop jujeopton || true
            docker rm jujeopton || true
            docker run -d --name jujeopton --env-file /home/gooch123/env/.env.dev -p 4000:3000 gooch1744/jujeopton:latest

Github Secret

이제 본인의 프로젝트가 있는 깃허브 리포지토리로 가보자. 

  리포지토리의 세팅으로 이동

Secrets and variables의 Actions로 이동

New repository secret을 클릭하자.
secret이 무엇이냐? API key나 비밀번호같은 민감한 정보를 깃허브에 통째로 올리면 보안상의 위협이 있을 수 있으니 action을 통해 주입할수 있도록 제공하는 기능이다.
여기에 위의 yml 파일에서 넣어줬던 5개의 시크릿을 만들어줘야 한다.

  1. DOCKER_USERNAME : 도커 계정의 아이디
  2. DOCKER_PASSWORD : 도커 계정의 비밀번호
  3. HOST : 접속할 서버의 IP 혹은 도메인
  4. USERNAME : 접속할 서버의 계정
  5. SSH_KEY : 접속할 서버에 저장되어있는 public key에 대응되어있는 private key, 다시 말하지만 ssh를 쓰기 싫으면, 접속 서버 계정의 비밀번호를 입력해도 된다.

그리고 참고로 개발 서버에는 도커가 당연히 설치되어 있어야 한다. 설치 과정은 GPT의 답변으로 대체하겠다

더보기

Ubuntu에서 Docker를 설치하려면 다음 단계를 따르세요. 아래는 공식 권장 방식입니다.


✅ 1. 기존 Docker 제거 (이미 설치된 경우)

sudo apt-get remove docker docker-engine docker.io containerd runc

✅ 2. 필요한 패키지 설치

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

✅ 3. Docker GPG 키 추가

sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

✅ 4. Docker 저장소 설정

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

✅ 5. Docker 엔진 설치

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

✅ 6. Docker가 잘 설치됐는지 확인

sudo docker version
sudo docker run hello-world

✅ 7. (선택) sudo 없이 docker 사용하고 싶다면

sudo usermod -aG docker $USER
newgrp docker

이후 터미널을 다시 시작하거나 newgrp docker 입력하면 sudo 없이 docker 명령 사용 가능


✅ 설치 확인 요약

docker --version
docker compose version

 

 
여기까지 설치하고 나면, main 브랜치에 변경사항을 push하거나 merge 할 때마다 자동으로 도커 이미지를 새로 빌드해서 도커 허브에 푸시한 다음, 원격으로 서버에 접속에 이미지를 pull 받아서 기존의 컨테이너를 내리고 새 컨테이너를 실행시켜준다.
 
그 과정은 리포지토리의 action 탭에 가면 볼 수 있다 !!

 

야 너두 배포 자동화 할수있어

정소민fan
@정소민fan :: 코딩은 관성이야

코딩은 관성적으로 해야합니다 즐거운 코딩 되세요

목차