GitHub Actions을 활용한 도커 이미지 빌드 및 EC2에 배포하는 과정

2025. 7. 31. 01:24·개발

⚙️ 1부: GitHub Actions - CI/CD 파이프라인 구축

GitHub Actions는 GitHub 저장소에서 직접 CI/CD 워크플로우를 자동화할 수 있게 해주는 도구입니다. 코드가 푸시될 때마다 자동으로 Docker 이미지를 빌드하고 Docker Hub에 푸시한 다음, AWS EC2 인스턴스에 배포하도록 설정합니다.

이번 섹션에서는 빌드(Build)와 배포(Deploy) 스텝을 명확히 분리하여, 각 단계의 역할과 발생할 수 있는 문제 해결 방안을 상세히 설명합니다.

1.1 워크플로우 파일 (.github/workflows/deploy.yml)

아래는 Docker 이미지를 빌드하고 Docker Hub에 푸시하는 build Job과, 이 이미지를 EC2에 배포하는 deploy Job으로 구성된 GitHub Actions 워크플로우 파일입니다.

YAML
 
name: Build and Deploy with Docker Compose

on:
  push:
    branches: [main]
  workflow_dispatch: # 수동 실행도 가능하게

env:
  IMAGE_NAME: ${{ secrets.DOCKER_HUB_USERNAME }}/shook # 실제 사용자명으로 변경
  DEPLOY_PATH: ~/shook-deploy

jobs:
  build: # 빌드 및 Docker Hub 푸시 Job
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

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

    - name: Login to Docker Hub
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_HUB_USERNAME }}
        password: ${{ secrets.DOCKER_HUB_TOKEN }}

    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64
        push: true
        tags: |
          ${{ env.IMAGE_NAME }}:latest
          ${{ env.IMAGE_NAME }}:${{ github.sha }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

    - name: Image digest
      run: echo "Image pushed successfully"

  deploy: # EC2 배포 Job
    needs: build # build Job이 성공해야 deploy Job이 실행됨
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code # EC2로 보낼 docker-compose.yml 파일에 접근하기 위해 필요
        uses: actions/checkout@v4

      - name: Deploy to EC2 with Docker Compose
        uses: appleboy/ssh-action@master # 최신 버전 사용 권장 (v0.1.7은 source/target 지원 안 함)
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          timeout: 300s
          sync: true # 파일 동기화 활성화
          source: "./docker-compose.yml" # 로컬 레포지토리의 docker-compose.yml
          target: "${{ env.DEPLOY_PATH }}" # EC2 서버의 대상 디렉토리

          script: |
            echo "🚀 Starting deployment process..."
            echo "Current user: $(whoami)"
            echo "Initial working directory: $(pwd)" # 스크립트 시작 시 현재 경로 확인

            # 배포 디렉토리 확인 및 생성
            echo "DEBUG: Checking and creating deployment directory: ${{ env.DEPLOY_PATH }}"
            mkdir -p "${{ env.DEPLOY_PATH }}" || { echo "ERROR: Failed to create directory ${{ env.DEPLOY_PATH }}. Check permissions for $(dirname ${{ env.DEPLOY_PATH }})"; exit 1; }
            
            # 배포 디렉토리로 이동
            cd "${{ env.DEPLOY_PATH }}" || { echo "ERROR: Failed to change directory to ${{ env.DEPLOY_PATH }}. Does it exist and have correct permissions?"; exit 1; }
            echo "DEBUG: Current working directory AFTER CD: $(pwd)" # cd 후 현재 경로 확인
            
            # 환경변수 파일 생성
            echo "📝 Attempting to create environment file (.env)..."
            cat > .env << EOF || { echo "ERROR: Failed to write to .env file. Check permissions for ${{ env.DEPLOY_PATH }} or disk space."; exit 1; }
            SESSION_SECRET=${{ secrets.SESSION_SECRET }}
            DATABASE_URL=${{ secrets.DATABASE_URL }}
            YOUTUBE_API_KEY=${{ secrets.YOUTUBE_API_KEY }}
            SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}
            SLACK_CHANNEL_ID=${{ secrets.SLACK_CHANNEL_ID }}
            SLACK_BOT_USER_OAUTH_TOKEN=${{ secrets.SLACK_BOT_USER_OAUTH_TOKEN }}
            ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}
            EOF
            echo "DEBUG: Current working directory AFTER .ENV CREATION: $(pwd)"
            
            # .env 파일 생성 여부 및 내용 확인 (🚨 디버깅 후에는 반드시 제거/주석 처리하세요!)
            echo "DEBUG: Verifying .env file existence and content:"
            ls -la .env || { echo "ERROR: .env file was NOT created or is inaccessible!"; exit 1; }
            cat .env # 🚨 시크릿 정보가 로그에 출력됩니다!
            
            # Docker 로그인
            echo "🔐 Logging into Docker Hub..."
            echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin || { echo "ERROR: Docker login failed."; exit 1; }
            echo "DEBUG: Current working directory AFTER DOCKER LOGIN: $(pwd)"
            
            # docker-compose.yml 파일 유효성 검사 (이제 파일이 있을 것입니다!)
            echo "DEBUG: Validating docker-compose.yml file..."
            cat docker-compose.yml # 🚨 docker-compose.yml 파일 내용 출력!
            docker compose config || { echo "ERROR: docker-compose.yml validation failed. Check YAML syntax (indentation, tabs/spaces, hidden characters)."; exit 1; }
            echo "DEBUG: Current working directory AFTER DOCKER COMPOSE CONFIG: $(pwd)"
            
            # 최신 이미지 풀 (docker pull 대신 docker compose pull 권장)
            echo "📥 Pulling latest image..."
            docker compose pull || { echo "ERROR: Docker Compose pull failed."; exit 1; }
            echo "DEBUG: Current working directory AFTER DOCKER COMPOSE PULL: $(pwd)"
            
            # 기존 서비스 중지
            echo "⏹️ Stopping existing services..."
            docker compose down || echo "DEBUG: No existing services to stop, or graceful shutdown failed." 
            echo "DEBUG: Current working directory AFTER DOCKER COMPOSE DOWN: $(pwd)"
            
            # 새 서비스 시작
            echo "▶️ Starting new services..."
            docker compose up -d || { echo "ERROR: Docker Compose up failed. Check application logs for details."; exit 1; }
            echo "DEBUG: Current working directory AFTER DOCKER COMPOSE UP: $(pwd)"
            
            # 잠시 대기
            echo "⏳ Waiting for services to start..."
            sleep 15
            
            # 컨테이너 상태 확인
            echo "✅ Checking container status..."
            docker compose ps || { echo "ERROR: Docker Compose ps failed. Services might not be running."; exit 1; }
            echo "DEBUG: Current working directory AFTER DOCKER COMPOSE PS: $(pwd)"
            
            # 애플리케이션 로그 확인
            echo "📋 Recent application logs:"
            docker compose logs --tail 20 app || echo "DEBUG: Could not retrieve recent application logs."
            echo "DEBUG: Current working directory AFTER DOCKER COMPOSE LOGS: $(pwd)"
            
            # 헬스체크
            echo "🔍 Performing health check..."
            for i in {1..10}; do
              if curl -f http://localhost:3000/health 2>/dev/null; then
                echo "✅ Application is healthy!"
                break
              elif [ $i -eq 10 ]; then
                echo "❌ Health check failed after 10 attempts"
                docker compose logs app || echo "DEBUG: Could not retrieve application logs for healthcheck failure."
                exit 1
              else
                echo "⏳ Attempt $i: Waiting for application to be ready..."
                sleep 3
              fi
            done
            echo "DEBUG: Current working directory AFTER HEALTH CHECK: $(pwd)"
            
            # 사용하지 않는 이미지 정리
            echo "🧹 Cleaning up unused images..."
            docker image prune -f || echo "DEBUG: Image prune command failed or nothing to prune."
            echo "DEBUG: Current working directory AFTER IMAGE PRUNE: $(pwd)"
            
            echo "🎉 Deployment completed successfully!"

  notify:
    needs: [build, deploy]
    runs-on: ubuntu-latest
    if: always()
    
    steps:
    - name: Notify deployment result
      run: |
        if [ "${{ needs.deploy.result }}" == "success" ]; then
          echo "✅ Deployment successful"
        else
          echo "❌ Deployment failed"
          exit 1
        fi

1.2 GitHub Secrets 설정 (매우 중요!)

민감한 정보(Docker Hub 비밀번호, SSH 키 등)는 코드에 직접 노출되어서는 안 됩니다. GitHub Secrets에 안전하게 저장하고 워크플로우에서 참조합니다.

  1. Docker Hub Personal Access Token (PAT) 생성:
    • Docker Hub에 로그인 후, Account Settings > Security > New Access Token에서 PAT를 생성합니다.
    • 권한은 Read & Write로 설정하고, 생성 즉시 토큰을 복사합니다. (다시 볼 수 없음)
  2. GitHub 저장소 Secrets 등록:
    • GitHub 저장소 Settings > Secrets and variables > Actions로 이동합니다.
    • New repository secret을 클릭하여 다음 Secret들을 등록합니다.
      • DOCKER_HUB_USERNAME: 여러분의 Docker Hub 사용자 이름
      • DOCKER_HUB_TOKEN: 위에서 생성한 Docker Hub PAT
      • EC2_HOST: EC2 인스턴스의 퍼블릭 IP 주소 또는 도메인
      • EC2_USERNAME: EC2 인스턴스의 사용자 이름 (예: ec2-user, ubuntu 등)
      • EC2_PRIVATE_KEY: EC2 인스턴스 생성 시 받은 .pem 파일(예: default-key-pair.pem)의 내용 전체를 복사하여 붙여넣습니다. (-----BEGIN ...부터 -----END ...까지 모두 포함)

1.3 GitHub Actions 관련 문제 해결

문제 1: ssh.ParsePrivateKey: ssh: no key found 또는 ssh: handshake failed: ssh: unable to authenticate

  • 증상: SSH 접속 시 키를 찾을 수 없거나 인증에 실패했다는 에러.
  • 원인:
    • EC2_PRIVATE_KEY Secret에 Private Key 내용이 잘못 복사되었거나 누락됨.
    • EC2 인스턴스의 ~/.ssh/authorized_keys 파일에 해당 Private Key에 맞는 Public Key가 없거나 권한 문제.
    • username (예: ubuntu, ec2-user)이 EC2 인스턴스의 실제 사용자 이름과 불일치.
  • 해결:
    • EC2_PRIVATE_KEY Secret에 .pem 파일의 내용을 정확히, 모든 줄을 포함하여 복사했는지 재확인.
    • EC2 인스턴스 생성 시 사용한 .pem 파일은 Public Key가 자동으로 등록되므로, authorized_keys를 수동으로 수정할 필요는 없습니다.
    • 워크플로우의 username을 ec2-user (또는 해당 AMI의 기본 사용자)로 정확히 설정했는지 확인.

☁️ 2부: EC2 배포 - 컨테이너 실행 환경

GitHub Actions가 Docker Hub에 이미지를 푸시하면, 이제 EC2 인스턴스에서 이 이미지를 가져와 실행해야 합니다.

2.1 EC2 환경 설정

  1. EC2 인스턴스 접속: SSH를 사용하여 EC2 인스턴스에 접속합니다.
  2. Docker 및 Docker Compose 설치: Amazon Linux 2023 환경에서 Docker 및 Docker Compose v2를 설치하는 방법은 다음과 같습니다.
  3. Bash
     
    # Docker 설치
    sudo dnf update -y
    sudo dnf install -y docker
    sudo systemctl start docker
    sudo systemctl enable docker
    sudo usermod -aG docker ec2-user
    newgrp docker # 그룹 변경 적용 (재접속 필요)
    
    # Docker Compose v2 설치 (공식 binary 다운로드)
    DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
    mkdir -p $DOCKER_CONFIG/cli-plugins
    
    curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 \
      -o $DOCKER_CONFIG/cli-plugins/docker-compose
    
    chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
    
    # 정상 설치 확인
    docker compose version
    
  4. 배포 디렉토리 생성:
  5. Bash
     
    mkdir -p /home/ec2-user/shook-deploy # EC2_USERNAME에 맞게 경로 조정
    cd /home/ec2-user/shook-deploy
    
  6. 환경 변수 파일 (.env): GitHub Actions에서 .env 파일을 직접 생성하고 Secret 값을 주입하도록 워크플로우를 구성했으므로, EC2에 수동으로 환경 변수 파일을 생성할 필요는 없습니다. 워크플로우의 deploy 스텝에서 자동으로 생성됩니다.

🚧 3부: Docker Compose 배포 문제 해결 (추가)

만약 docker-compose를 사용하여 배포를 시도했다면, 다음과 같은 유효성 검사 오류를 겪을 수 있습니다.

문제 1: bash: line 28: docker-compose: command not found

  • 증상: docker-compose 명령어를 찾을 수 없다는 에러.
  • 원인: EC2 인스턴스에 docker-compose가 설치되어 있지 않거나, PATH에 등록되지 않았기 때문입니다.
  • 해결: 위에 제시된 방법으로 EC2에 docker compose v2를 설치합니다. (docker-compose는 구 버전 바이너리 이름이며, 최신 docker compose v2는 docker compose로 실행됩니다.)

문제 2: validating /home//shook-deploy/docker-compose.yml: services.healthcheck additional properties 'interval', 'timeout', 'retries', 'start_period', 'test' not allowed

문제 3: validating /home//shook-deploy/docker-compose.yml: services.logging additional properties 'options', 'driver' not allowed

      • 증상: docker-compose.yml 파일의 healthcheck 또는 logging 섹션에서 "additional properties 'X' not allowed" 오류 발생.
      • 원인: docker-compose.yml 파일의 version: '3.x' 선언과 EC2에 설치된 docker compose 바이너리(v2.x) 간의 불일치. 최신 docker compose 바이너리는 Compose Specification v2를 따르는데, v3.x 구문과 일부 필드의 구조가 다릅니다. 이전에 발생했던 문제의 주된 원인은 YAML 들여쓰기/인코딩 오류였지만, 만약 파일이 올바르게 전송된 후에도 동일한 에러가 발생한다면 이 문제를 의심해봐야 합니다.
      • 해결: docker-compose.yml 파일의 version 필드를 제거하거나 version: '2.x'로 변경하고, healthcheck 및 logging 구문을 Compose Specification v2에 맞게 수정해야 합니다.
        YAML
         
        # docker-compose.yml (수정 예시)
        # version: '3.8' # 이 줄을 제거하거나 version: '2.x' 로 변경
        
        services:
          app: # 서비스 이름을 shook-app 대신 app으로 통일 (사용하는 서비스명에 따라 변경)
            image: your-docker-hub-username/shook:latest
            ports:
              - "80:3000"
            environment:
              - DATABASE_URL=${DATABASE_URL} # .env 파일에서 주입될 변수
              # ... (다른 환경 변수) ...
            healthcheck: # healthcheck 구문
              test: ["CMD", "curl", "-f", "http://localhost:3000/health"] # 컨테이너 내부에서 실행될 헬스체크 명령어
              interval: 30s
              timeout: 10s
              retries: 3
              start_period: 5s
            logging: # logging 구문
              driver: "json-file"
              options:
                max-size: "10m"
                max-file: "3"
        

         

      • 예시 (Compose Specification v2에 맞는 healthcheck 및 logging 구문):
        # docker-compose.yml (수정 예시)
        # version: '3.8' # 이 줄을 제거하거나 version: '2.x' 로 변경
        
        services:
          app: # 서비스 이름을 shook-app 대신 app으로 통일 (사용하는 서비스명에 따라 변경)
            image: your-docker-hub-username/shook:latest
            ports:
              - "80:3000"
            environment:
              - DATABASE_URL=${DATABASE_URL} # .env 파일에서 주입될 변수
              # ... (다른 환경 변수) ...
            healthcheck: # healthcheck 구문
              test: ["CMD", "curl", "-f", "http://localhost:3000/health"] # 컨테이너 내부에서 실행될 헬스체크 명령어
              interval: 30s
              timeout: 10s
              retries: 3
              start_period: 5s
            logging: # logging 구문
              driver: "json-file"
              options:
                max-size: "10m"
                max-file: "3"

'개발' 카테고리의 다른 글

Vite와 Node.js 풀스택 개발에서 npm run dev를 통해 프론트와 백 동시에 실행하기(디버깅)  (3) 2025.08.02
Node.js 앱 무중단 배포: GitHub Actions와 Docker로 EC2 자동화 파이프라인 구축하기  (5) 2025.08.02
Docker 컨테이너에서 Node.js 풀스택 앱 실행 시 흔한 문제 해결 가이드  (1) 2025.07.30
프롬프트 엔지니어링 작성 방법  (1) 2025.07.12
MCP 사용기(Claude Desktop을 활용하여 Firecrawl, slack 연동)  (4) 2025.07.05
'개발' 카테고리의 다른 글
  • Vite와 Node.js 풀스택 개발에서 npm run dev를 통해 프론트와 백 동시에 실행하기(디버깅)
  • Node.js 앱 무중단 배포: GitHub Actions와 Docker로 EC2 자동화 파이프라인 구축하기
  • Docker 컨테이너에서 Node.js 풀스택 앱 실행 시 흔한 문제 해결 가이드
  • 프롬프트 엔지니어링 작성 방법
5jyan5
5jyan5
  • 5jyan5
    jyan
    5jyan5
  • 전체
    오늘
    어제
    • 분류 전체보기 (242)
      • 김영한의 스프링 핵심 원리(기본편) (8)
      • 김영한의 스프링 핵심 원리 - 고급편 (11)
      • 김영한의 스프링 MVC 1편 (1)
      • 김영한의 스프링 DB 1편 (3)
      • 김영한의 스프링 MVC 2편 (3)
      • 김영한의 ORM 표준 JPA 프로그래밍(기본편) (9)
      • 김영한의 스프링 부트와 JPA 활용2 (2)
      • 김영한의 실전 자바 - 중급 1편 (1)
      • 김영한의 실전 자바 - 고급 1편 (9)
      • 김영한의 실전 자바 - 고급 2편 (9)
      • Readable Code: 읽기 좋은 코드를 작성.. (2)
      • 김영한의 실전 자바 - 고급 3편 (9)
      • CKA (118)
      • 개발 (37)
      • 경제 (4)
      • 리뷰 (1)
      • 정보 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      jdk 동적 프록시
      스레드
      양방향 맵핑
      버퍼
      @within
      reentarantlock
      빈 후처리기
      락
      @args
      조회 성능 최적화
      프록시 팩토리
      requset scope
      WAS
      단방향 맵핑
      jpq
      JPQL
      김영한
      페치 조인
      프록시
      typequery
      log trace
      @discriminatorcolumn
      @discriminatorvalue
      고급
      Thread
      hibernate5module
      자바
      cglib
      gesingleresult
      Target
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.2
    5jyan5
    GitHub Actions을 활용한 도커 이미지 빌드 및 EC2에 배포하는 과정
    상단으로

    티스토리툴바