본문 바로가기
DevOps

Github Action + Docker compose + Nginx로 무중단 배포 CI/CD 구축하기

by 태균맨 2025. 5. 8.

1. docker-compose green/blue 구성

services:
  nextjs-blue: # >>> 이부분 변경 <<<
    build:
      context: .
      dockerfile: Dockerfile
    container_name: nextjs-blue
    restart: always
    env_file:
      - .env
    networks:
      - app-network

  nextjs-green: # >>> 이부분 변경 <<<
    build:
      context: .
      dockerfile: Dockerfile
    container_name: nextjs-green
    restart: always
    env_file:
      - .env
    networks:
      - app-network
      
        nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot
    depends_on:
      - nextjs-blue # >>> 이부분 변경 <<<
      - nextjs-green # >>> 이부분 변경 <<<
    networks:
      - app-network
    command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"'''
    
    
    networks:
  app-network:
    driver: bridge

docker-compose에서 기존 nextjs로 구성된 서비스를 nextjs-blue, nextjs-green으로 변경 후 nginx에 의존성 추가 해줬다.

 

2. nginx 설정 변경

# >>> 이부분 추가 <<<

upstream nextjs_upstream {
    server nextjs-blue:3000; 
}

server {
    listen 80;
    server_name ~~~~~~~~~~~~~~~~~~

    client_max_body_size 100M;
    
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
        allow all;
    }
    
    location / {
        proxy_pass http://nextjs_upstream # >>> 이부분 추가 <<<
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

server {
    listen 443 ssl;
    server_name ~~~~~~~~~~~~
    
    client_max_body_size 100M;

    
    location / {
        proxy_pass http://nextjs_upstream; # >>> 이부분 추가 <<<
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Nginx가 트래픽을 어디로 보낼 지 하는 설정입니다. (기본은 블루로 해줬다.)

3. sh 작성

#!/bin/bash

# 현재 활성화된 서비스 확인
CURRENT_SERVICE=$(grep -v "^#" nginx.conf | grep -o "nextjs-[a-z]*" | head -1)

if [ "$CURRENT_SERVICE" == "nextjs-blue" ]; then
    # Blue가 활성화되어 있으면 Green으로 배포
    TARGET_SERVICE="nextjs-green"
    IDLE_SERVICE="nextjs-blue"
else
    # Green이 활성화되어 있으면 Blue로 배포
    TARGET_SERVICE="nextjs-blue"
    IDLE_SERVICE="nextjs-green"
fi

echo "현재 서비스: $CURRENT_SERVICE"
echo "배포 대상 서비스: $TARGET_SERVICE"

# 대상 서비스 컨테이너와 이미지 제거
docker compose rm -f $TARGET_SERVICE
docker rmi $(docker images | grep $TARGET_SERVICE | awk '{print $3}') 2>/dev/null || true

# Docker 시스템 정리 (캐시된 이미지와 볼륨 정리)
echo "Docker 시스템 정리 중..."
docker system prune -f

# nginx가 실행 중인지 확인하고 실행되지 않았다면 시작
NGINX_RUNNING=$(docker compose ps | grep nginx | grep Up || echo "")
if [ -z "$NGINX_RUNNING" ]; then
    echo "nginx 컨테이너 시작 중..."
    docker compose up -d nginx
    sleep 5
fi

# 대상 서비스 빌드 및 시작
echo "$TARGET_SERVICE 빌드 및 시작 중..."
docker compose up -d --build --force-recreate $TARGET_SERVICE

# 새 서비스가 시작되었는지 확인
echo "새 서비스가 시작되기를 기다리는 중..."
sleep 15  # 시간 증가
CONTAINER_RUNNING=$(docker compose ps | grep $TARGET_SERVICE | grep Up || echo "")
if [ -z "$CONTAINER_RUNNING" ]; then
    echo "오류: $TARGET_SERVICE 컨테이너가 시작되지 않았습니다."
    echo "로그 확인 중..."
    docker compose logs $TARGET_SERVICE
    # 오류 발생 시 롤백 로직 추가
    echo "이전 서비스로 롤백합니다..."
    exit 1
fi

# nginx.conf 업데이트
echo "nginx.conf 업데이트 중..."
sed -i "s/$IDLE_SERVICE/$TARGET_SERVICE/g" nginx.conf
sed -i "s/# server $TARGET_SERVICE/server $TARGET_SERVICE/g" nginx.conf
sed -i "s/server $IDLE_SERVICE/# server $IDLE_SERVICE/g" nginx.conf

# 변경된 설정 확인
echo "변경된 nginx 설정:"
cat nginx.conf

# nginx 컨테이너 재시작 (리로드 대신)
echo "nginx 컨테이너 재시작 중..."
docker compose restart nginx

# 재시작 후 상태 확인
sleep 3
NGINX_RUNNING=$(docker compose ps | grep nginx | grep Up || echo "")
if [ -z "$NGINX_RUNNING" ]; then
    echo "오류: nginx 재시작 실패"
    exit 1
else
    echo "nginx가 성공적으로 재시작되었습니다."
fi

echo "트래픽이 $TARGET_SERVICE로 전환되었습니다."

# 이전 서비스는 일정 시간 후 중지
sleep 30
echo "이전 서비스를 중지합니다..."
docker compose stop $IDLE_SERVICE

주석 참조 바람...

대충 요약하면 Blue일땐 Green으로 배포 Green일 때는 Blue로 배포 후

2에서 설정한 Nginx 트래픽 변경 후 재시작

 

여기까지 성공하면 Green/Blue 방식의  무중단 배포까진 성공했다.

4. 프로젝트에 .github/workflows/.yml 파일 작성

# deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH and deploy
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /home/여러분의 프로젝트 경로~
            git stash
            git pull
            chmod +x ./deploy.sh
            ./deploy.sh

Deploy라는 워크플로우를 작성해줬다.

 

여기서 중요한 건

secrets.HOST

secrets.USERNAME

secrets.SSH_PRIVATE_KEY

세 개와

script: 아래 코드다

 

main으로 푸시하면 위에 시크릿 정보로 ssh 접속 후 스크립트를 자동 실행한다!

 

5. github에 Repository secrets 추가

깃허브 프로젝트 내 Settings

 

Security - Secrets and variables - Actions
이렇게 세개 추가해주자

 

 

제 서버 기준 SSH_PRIVATE_KEY는 이렇게 생성했습니다.

ssh-keygen -t rsa -b 4096 -C "github-actions"

빨간색 박스는 기본 경로로 저장 [Enter] / 파란색 박스는 비밀번호 사용이지만 Github에서 사용할거니까 입력 하지 말고 [Enter]

 

sudo nano /etc/ssh/sshd_config

SSH 설정 파일에서

 

PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

주석 해제

주석 해제

sudo systemctl restart sshd

SSH 서비스 재시작

 

cat ~/.ssh/id_rsa

시크릿 키 확인

 

이 부분을 전부 복사해서 붙여 주면 됩니다.

-----BEGIN OPENSSH PRIVATE KEY-----
SSH PRIVATE KEY~~~~~~~~~~~~~
-----END OPENSSH PRIVATE KEY-----

 

 

 

 


 

자 이제 위에 설정한 브런치로 push를 해보면!!!!!!!

 

위에부터 성공, 실패, 캔슬..

 

클릭해보면 상세 로그도 볼 수 있습니다.

 

그리고,, 시행 착오들..