DevOps

Docker를 이용하여 EC2에 Node 서버 배포하기 (2)

MIRACLE LIFE 2026. 5. 11. 19:00

이전 글에서는 아래 과정을 직접 수동으로 진행했다.

Docker 이미지 빌드 -> ECR Push -> EC2 Pull -> docker run

하지만 매번 직접 명령어를 입력하는 것은 번거로운 일이다.

 

이번 글에서는 GitHub Actions를 사용해서 Node 서버를 자동 배포하는 과정을 정리한다.

 

지난 글

https://geonbbang.tistory.com/74

 

Docker를 이용하여 EC2에 Node 서버 배포하기 (1)

Node 서버를 운영하다 보면 다음과 같은 문제들이 생길 수 있다.배포를 했더니 개발 환경과 운영 환경이 같지 않아서 문제가 발생한다.다른 개발자와의 개발 환경이 달라 충돌이 발생한다.가끔

geonbbang.tistory.com

 

 

 

전체 흐름은 아래와 같다.

  1. GitHub Actions 실행
    1. Docker 이미지 빌드
    2. AWS ECR Push
    3. EC2 스크립트 실행
  2. EC2 스크립트 작성
  3. docker-compose.yml 작성

GitHub Actions 실행

GitHub Actions 실행을 위해 아래 경로에 workflow 파일을 작성한다.

.github/workflows/deploy.yml

name: Build + Deploy

on:
  push:
    branches: [dev]

jobs:
  build-analyze-deploy:
    runs-on: ubuntu-latest

    permissions:
      id-token: write
      contents: read
      pull-requests: write
      checks: write

    steps:
      # 코드 체크아웃
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # Node 세팅
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 24

      # 의존성 설치
      - name: Install deps
        run: npm install

      # AWS 인증 (OIDC)
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-2

      # ECR 로그인
      - name: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # Docker build & push
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ steps.login-ecr.outputs.registry }}/geonbbang/sample-app:latest
            ${{ steps.login-ecr.outputs.registry }}/geonbbang/sample-app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          retries: 2

      # EC2 배포
      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            bash ~/app/deploy.sh

 

이 workflow가 실행되면 전체 흐름은 아래처럼 동작한다.

 

GitHub Push
→ GitHub Actions 실행
→ Docker 이미지 Build
→ ECR Push
→ EC2 접속
→ deploy.sh 실행
→ Blue/Green 배포

 


deploy.sh 작성

GitHub Actions의 마지막 단계에서는 아래 명령이 실행된다.

script: |
  bash ~/app/deploy.sh

 

즉, EC2 내부에서 실제 배포를 수행하는 역할을 하는 것이다.

 

 

deploy.sh

#!/bin/bash

cd ~/app

STATE_FILE=~/app/deploy_state

CURRENT=$(cat $STATE_FILE)

if [ "$CURRENT" = "blue" ]; then
  NEW=green
  NEW_PORT=3001
  OLD=blue
  OLD_PORT=3000
else
  NEW=blue
  NEW_PORT=3000
  OLD=green
  OLD_PORT=3001
fi

echo "Deploying $NEW..."

echo "Logging in to ECR..."

aws ecr get-login-password \
--region ap-northeast-2 \
| docker login \
--username AWS \
--password-stdin {ECR_URL}

# 최신 이미지 pull
docker compose pull

# 새 컨테이너 실행
docker compose up -d app-$NEW

# =========================
# HEALTH CHECK
# =========================
echo "Health checking..."

for i in {1..10}; do
  if curl -f http://localhost:$NEW_PORT/health; then
    echo "Healthy!"
    break
  fi
  sleep 2
done

# 실패 시 롤백
if ! curl -f http://localhost:$NEW_PORT/health; then
  echo "Deploy failed → rollback"
  docker compose stop app-$NEW
  exit 1
fi

# =========================
# NGINX SWITCH
# =========================
echo "Switching nginx → $NEW_PORT"

sudo sed -i "s/$OLD_PORT/$NEW_PORT/g" /etc/nginx/sites-available/sample-app
sudo nginx -s reload

# 상태 저장
echo "$NEW" > $STATE_FILE

# 기존 컨테이너 종료
docker compose stop app-$OLD

echo "Deploy complete"

 

이 스크립트의 역할은 다음과 같다.

현재 서비스 중인 컨테이너는 그대로 둔 상태에서, 반대쪽 컨테이너에 새 버전 앱을 먼저 실행한다.

새 버전이 정상인지 헬스체크로 확인한 뒤, Nginx 설정을 바꿔 트래픽을 새 버전으로 전환한다.

전환이 끝나면 이전 컨테이너를 종료하고, 문제가 있으면 기존 서비스는 유지한 채 배포만 실패 처리 한다.


deploy_state 작성

다음은 현재 어떤 컨테이너가 운영 중인지 저장하는 파일이다.

 

deploy_state

blue

 

이 파일은 굉장히 단순하지만 Blue/Green 배포에서 핵심 역할을 한다.

배포 스크립트는 이 값을 기준으로:

  • 현재 blue 운영 중 → green 배포
  • 현재 green 운영 중 → blue 배포

를 결정한다.

 


docker-compose.yml 작성

다음은 실제 컨테이너를 정의하는 파일이다.

services:
  app-blue:
    image: {ECR_URL}/geonbbang/sample-app:latest
    container_name: app-blue
    command: node dist/main
    ports:
      - "3000:3000"
    env_file:
      - .env
    restart: always

  app-green:
    image: {ECR_URL}/geonbbang/sample-app:latest
    container_name: app-green
    command: node dist/main
    ports:
      - "3001:3000"
    env_file:
      - .env
    restart: always

 

왜 컨테이너를 2개 사용하는가?

Blue/Green 배포의 핵심은 현재 운영 중인 서버를 유지한 상태로 새 버전을 띄우는 것

즉 현재 app-blue 가 운영 중이라면 app-green 에 새 버전을 먼저 배포한다.

새 서버가 정상이라면 nginx 트래픽을 전환하고, 이후 기존 서버를 종료한다.

 


마무리

이렇게 구성함으로써

- 수동 배포 제거

- 무중단 배포

- 자동 롤백 기반 확보

- 운영 안정성 향상

의 효과를 챙길 수 있게 되었다.