클라우드 4단계: Docker 컨테이너화 배포 - 100-hours-a-week/16-Hot6-wiki GitHub Wiki

상위 문서: 클라우드 위키
관련 문서: 클라우드 WHY? 문서

Docker 컨테이너화 배포

개요

이 문서는 기존 인스턴스 로컬 실행 기반 서비스 배포 구조를 개선하기 위해 Docker를 도입한 과정을 정리한 기술 문서입니다.
환경별 개발 편차, 수동 배포의 반복, 운영 환경과 로컬 환경 간의 불일치 등 다양한 문제를 해결하고자
컨테이너 기반의 일관된 실행 환경과 자동화된 배포 구조를 구축하였습니다.

Docker 도입 이후, Dev/Prod 환경을 분리하고 GitHub Actions 기반 CI/CD를 구성했으며,
Blue-Green 방식의 포트 기반 배포 전략을 통해 롤백 가능성과 서비스 무중단 배포를 확보하였습니다.

본 문서에서는 Docker 도입 배경과 그 선택 기준부터 시작하여,
컨테이너 구조 설계, 이미지 태그 전략, 환경 변수 관리 방식,
Dev/Prod 배포 자동화 워크플로우, Blue-Green 배포 및 롤백 구조까지 전체 흐름을 단계별로 설명합니다.

앞으로의 EKS 전환 계획과 함께, 이 컨테이너화 전략은 팀의 운영 효율성과 서비스 확장성을 동시에 고려한 핵심 인프라로 작동하게 될 것입니다.


목차


Docker 도입 결정 및 범위

Docker 도입 결정 및 범위 요약

  • 🔁 Blue-Green 배포에서 인스턴스 전체를 교체하지 않고 포트 단위로 컨테이너를 전환할 수 있게 되었음. 이 구조는 기존 인프라 기반의 배포보다 훨씬 빠르고 안전하며, 롤백 또한 컨테이너 단위로 손쉽게 수행할 수 있음
  • 이 구조적 이점은 Terraform 기반의 인프라 관리와도 충돌 없이 조화롭게 작동하며, CD 자동화의 핵심 기반이 됨.
  • 개발자마다 환경이 달라 생기던 문제(Python 버전, 경로 불일치 등)를 Docker가 해결해 줌
  • 운영 환경과 동일한 컨테이너 기반으로 로컬 테스트 가능 → 재현성과 안정성 확보
  • 특히 Python 기반 FastAPI는 의존성 복잡도가 높아 Docker와 궁합이 뛰어났음
  • GitHub Actions, 로컬, 서버 등 전 구간에서 동일 환경을 보장하는 기준점 역할

Docker는 단순한 실행 도구가 아니라, 우리 팀의 CD 전략을 완성하고 개발-배포 체계를 안정화하는 핵심 인프라였습니다.

  • docker run에 비해 구조와 실행 방식을 쉽게 공유 가능 → 처음 보는 개발자도 이해 쉬움
  • .envSecret Manager 기반 설정 주입이 용이
  • dev, prod 환경별 설정 분리도 자연스럽게 지원 (e.g. docker-compose.override.yml)

"docker-compose는 개발자/협업 관점에서 가독성과 유연성을 제공합니다."

  • GitHub Actions secrets는 제한적이며 CI 전용, 반면 Secret Manager는 앱, 인프라, 배포 전반에서 사용 가능
  • GCP/AWS 모두 유사한 구조(버전 관리, IAM, JSON 기반)를 제공 → 멀티클라우드 이식성 우수
  • .env 렌더링, docker-compose 연동, CI 자동화 등 다양한 환경에서 통합 활용
  • Cloud 환경에서도 보안 감사를 통과할 수 있는 권한 분리·로그 관리·버전 롤백 기능 내장

Secret Manager는 단순 보안 도구가 아닌, 인프라 수준에서 신뢰 가능한 보안 구성 요소였습니다.

컨테이너 구조 다이어그램

image

왜 prod에서 HTTPS LB와 Nginx를 동시에 사용하나요?

왜 테라폼을 도입했나요?

Docker 도입 전략 설명서

1. 컨테이너 태그 전략

컨테이너 이미지 태그는 버전 관리, 배포 트리거, 롤백 가능성 등을 좌우하는 핵심 전략입니다.
저희는 다음과 같은 기준으로 태그를 설계했습니다.

기본 전략: 환경 + 서비스 단위 구분

이미지 dev 태그 prod 태그
프론트엔드 (Next.js) dev-frontend frontend
백엔드 (Spring) dev-backend backend
AI 서버 (FastAPI) dev-ai ai
  • dev-*: dev 브랜치 push마다 자동으로 덮어쓰기 → 최신 개발 상태 반영
  • prod: main 브랜치에서 머지되면 버전 + latest를 함께 태깅 (예: backend:1.2.0, backend:latest)

prod 태그 규칙: 버전 + latest

  • 1.0.0, 1.1.0, 1.2.1 등의 세미버전 태그
  • 동시에 latest로도 push하여 default pull 대응
# 예: prod 백엔드 배포 시 이미지 생성
docker build -t onthe-top/backend:1.2.0 -t onthe-top/backend:latest .
docker push onthe-top/backend:1.2.0
docker push onthe-top/backend:latest
  • 버전 태그는 롤백을 가능하게 함
  • latest는 사람이 명시하지 않아도 디폴트로 잡히도록 함

dev 태그 규칙: 단일 덮어쓰기

  • dev-backend, dev-ai, dev-frontend는 각 브랜치 push마다 동일 태그로 덮어쓰기
  • 주로 dev 환경에서는 최신 상태 테스트에 의미가 있으므로, 별도 버전은 관리하지 않음
# 예: dev 백엔드 배포 시
docker build -t onthe-top/dev-backend .
docker push onthe-top/dev-backend

태그 전략 요약

항목 dev prod
목적 최신 개발 상태 테스트 안정적 버전 배포 및 롤백
버전 태그 없음 (단일 덮어쓰기) 세미버전 + latest 병행
사용 시점 PR 병합 시 자동 배포 릴리즈 시 수동 확인 후 배포
롤백 가능성 낮음 (재빌드 필요) 높음 (버전 단위 롤백 가능)

추가 고려: 이미지 사이즈 최적화

  • dev 이미지는 build-essential, debug, venv 등이 포함되어 다소 큼
  • prod 이미지는 multi-stage build, distroless, --no-cache 등을 통해 최적화
# dev 예시
FROM python:3.10
RUN pip install -r dev-requirements.txt

# prod 예시
FROM python:3.10-slim
COPY --from=builder /app /app

이 전략은 개발 속도와 운영 안정성을 모두 고려한 실용적인 구조입니다.
dev는 빠르게 덮어쓰기, prod는 확실한 버전 태깅으로 관리합니다.

Dev 환경용 GitHub Actions CD 워크플로우

Dev 환경은 다음과 같은 흐름으로 배포됩니다:

  1. 프론트엔드: S3 + CloudFront
  2. 백엔드/AI: Jump Server → Target Server로 SSH, 이후 docker-compose 실행

1. Frontend

name: Deploy FE to Dev

on:
  push:
    branches:
      - dev

jobs:
  deploy-fe:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Source
        uses: actions/checkout@v3

      - name: Build Frontend
        run: |
          cd frontend
          npm install
          npm run build

      - name: Upload to S3
        run: |
          aws s3 sync ./frontend/dist s3://${{ secrets.S3_BUCKET_DEV }} --delete
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ap-northeast-2

      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CLOUDFRONT_ID_DEV }} \
            --paths "/*"

2. Backend

name: Deploy BE to Dev

on:
  push:
    branches:
      - dev

jobs:
  deploy-be:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Source
        uses: actions/checkout@v3

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1
        with:
          credentials_json: '${{ secrets.GCP_SA_KEY }}'

      - name: Copy docker-compose.yml
        run: |
          scp -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              backend/docker-compose.yml \
              ec2-user@${{ secrets.BE_DEV_HOST }}:/home/ec2-user/dev/backend/docker-compose.yml

      - name: Fetch Secret from Secret Manager and Transfer
        run: |
          set -euo pipefail

          # Retrieve secret
          gcloud secrets versions access latest \
            --secret=dev-backend-env > backend.env

          # Mask all lines (invisible in logs)
          while IFS= read -r line; do
            echo "::add-mask::$line"
          done < backend.env

          # Transfer .env file via SSH
          scp -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              backend.env \
              ec2-user@${{ secrets.BE_DEV_HOST }}:/home/ec2-user/dev/backend/.env

          # Remove local file
          rm -f backend.env

      - name: Deploy Backend with Compose
        run: |
          ssh -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              ec2-user@${{ secrets.BE_DEV_HOST }} << 'EOS'
                set -euo pipefail
                cd /home/ec2-user/dev/backend
                docker-compose pull
                docker-compose down
                docker-compose up -d
          EOS

3. AI

name: Deploy AI to Dev

on:
  push:
    branches:
      - dev

jobs:
  deploy-ai:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Source
        uses: actions/checkout@v3

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1
        with:
          credentials_json: '${{ secrets.GCP_SA_KEY }}'

      - name: Copy docker-compose.yml
        run: |
          scp -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              ai/docker-compose.yml \
              ec2-user@${{ secrets.AI_DEV_HOST }}:/home/ec2-user/dev/ai/docker-compose.yml

      - name: Fetch Secret from Secret Manager and Transfer
        run: |
          set -euo pipefail

          # Secret 가져오기
          gcloud secrets versions access latest \
            --secret=dev-ai-env > ai.env

          # 로그 노출 방지를 위한 마스킹
          while IFS= read -r line; do
            echo "::add-mask::$line"
          done < ai.env

          # .env 전송
          scp -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              ai.env \
              ec2-user@${{ secrets.AI_DEV_HOST }}:/home/ec2-user/dev/ai/.env

          # 로컬 .env 제거
          rm -f ai.env

      - name: Deploy AI with Compose
        run: |
          ssh -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              ec2-user@${{ secrets.AI_DEV_HOST }} << 'EOS'
                set -euo pipefail
                cd /home/ec2-user/dev/ai
                docker-compose pull
                docker-compose down
                docker-compose up -d
          EOS

Prod 환경용 GitHub Actions CD 워크플로우

docker-compose.yaml

Prod 환경의 경우 BE와 AI가 한 번에 배포되기 때문에 하나의 파일에 작성되어 있습니다.
blue-green 전략에 맞추어 포트를 동적으로 받아서 사용할 수 있도록 구현했습니다.

version: '3.8'

services:
  backend:
    image: onthe-top/backend:latest
    container_name: ${BACKEND_CONTAINER_NAME}
    restart: always
    env_file:
      - .env
    ports:
      - "${BACKEND_HOST_PORT}:8080"  # ex) 8080:8080 또는 8081:8080
    networks:
      internal:
        aliases:
          - ${BACKEND_ALIAS}         # ex) backend-blue 또는 backend-green

  ai:
    image: onthe-top/ai:latest
    container_name: ${AI_CONTAINER_NAME}
    restart: always
    env_file:
      - .env
    ports:
      - "${AI_HOST_PORT}:8000"       # ex) 8000:8000 또는 8001:8000
    networks:
      internal:
        aliases:
          - ${AI_ALIAS}              # ex) ai-blue 또는 ai-green

networks:
  internal:

Prod 배포 스크립트

정확한 배포 플로우는 CD 문서의 CD 파이프라인 흐름도 섹션을 참고해 주세요.

name: Blue-Green Deploy to Production

on:
  workflow_dispatch:

jobs:
  deploy:
    name: Deploy to ${{ matrix.target }}
    runs-on: ubuntu-latest

    strategy:
      matrix:
        target_id: [prod1, prod2]

    env:
      BACKEND_BASE_PORT: 8080
      AI_BASE_PORT: 8000

    steps:
      - name: Checkout Source
        uses: actions/checkout@v3

      - name: Authenticate to GCP
        uses: google-github-actions/auth@v1
        with:
          credentials_json: '${{ secrets.GCP_SA_KEY }}'

      - name: Map Instance IP by Target ID
        id: map-ip
        run: |
          if [ "${{ matrix.target_id }}" = "prod1" ]; then
            echo "TARGET_IP=${{ secrets.PROD_INSTANCE_1 }}" >> $GITHUB_ENV
          elif [ "${{ matrix.target_id }}" = "prod2" ]; then
            echo "TARGET_IP=${{ secrets.PROD_INSTANCE_2 }}" >> $GITHUB_ENV
          else
            echo "❌ Unknown target_id: ${{ matrix.target_id }}"
            exit 1
          fi

      - name: Determine Free Ports on Target Host
        id: determine-port
        run: |
          BACKEND_PORT=$(ssh -o ProxyJump=ec2-user@$JUMP ec2-user@$TARGET '
            if ! ss -tulpn | grep -q ":8080"; then echo 8080;
            elif ! ss -tulpn | grep -q ":8081"; then echo 8081;
            else echo "error"; fi
          ')

          AI_PORT=$(ssh -o ProxyJump=ec2-user@$JUMP ec2-user@$TARGET '
            if ! ss -tulpn | grep -q ":8000"; then echo 8000;
            elif ! ss -tulpn | grep -q ":8001"; then echo 8001;
            else echo "error"; fi
          ')

          if [[ "$BACKEND_PORT" == "error" || "$AI_PORT" == "error" ]]; then
            echo "No free port available" && exit 1
          fi

          echo "BACKEND_PORT=$BACKEND_PORT" >> $GITHUB_ENV
          echo "AI_PORT=$AI_PORT" >> $GITHUB_ENV
          echo "TARGET=$TARGET" >> $GITHUB_ENV

          if [[ "$BACKEND_PORT" == "8080" ]]; then
            echo "BACKEND_NAME=backend-blue" >> $GITHUB_ENV
            echo "BACKEND_ALIAS=backend-blue" >> $GITHUB_ENV
          else
            echo "BACKEND_NAME=backend-green" >> $GITHUB_ENV
            echo "BACKEND_ALIAS=backend-green" >> $GITHUB_ENV
          fi

          if [[ "$AI_PORT" == "8000" ]]; then
            echo "AI_NAME=ai-blue" >> $GITHUB_ENV
            echo "AI_ALIAS=ai-blue" >> $GITHUB_ENV
          else
            echo "AI_NAME=ai-green" >> $GITHUB_ENV
            echo "AI_ALIAS=ai-green" >> $GITHUB_ENV
          fi

      - name: Pull Secrets from GCP Secret Manager
        run: |
          gcloud secrets versions access latest --secret=prod-backend-env > backend.env
          gcloud secrets versions access latest --secret=prod-ai-env > ai.env

          echo "BACKEND_HOST_PORT=${{ env.BACKEND_PORT }}" >> backend.env
          echo "BACKEND_CONTAINER_NAME=${{ env.BACKEND_NAME }}" >> backend.env
          echo "BACKEND_ALIAS=${{ env.BACKEND_ALIAS }}" >> backend.env

          echo "AI_HOST_PORT=${{ env.AI_PORT }}" >> ai.env
          echo "AI_CONTAINER_NAME=${{ env.AI_NAME }}" >> ai.env
          echo "AI_ALIAS=${{ env.AI_ALIAS }}" >> ai.env

      - name: Ensure Remote Directory Exists
        run: |
          ssh -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              ec2-user@$TARGET_IP "mkdir -p /home/ec2-user/prod"

      - name: Transfer docker-compose.yml and env files
        run: |
          scp -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              backend/docker-compose.yml \
              ec2-user@$TARGET_IP:/home/ec2-user/prod/docker-compose.yml

          scp -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              backend.env \
              ec2-user@$TARGET_IP:/home/ec2-user/prod/.backend.env

          scp -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              ai.env \
              ec2-user@$TARGET_IP:/home/ec2-user/prod/.ai.env

      - name: Deploy Backend and AI with docker-compose
        run: |
          ssh -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} ec2-user@${{ secrets.TARGET_IP }} << 'EOS'
            cd /home/ec2-user/prod

            echo "Deploying Backend..."
            docker-compose --env-file .backend.env up -d --force-recreate --remove-orphans backend

            echo "Deploying AI..."
            docker-compose --env-file .ai.env up -d --force-recreate --remove-orphans ai
          EOS

      - name: Generate Nginx Config File
        run: |
          cat > nginx.conf <<EOF
events {}

http {
  upstream backend {
    server 127.0.0.1:${{ env.BACKEND_PORT }};
  }

  upstream ai {
    server 127.0.0.1:${{ env.AI_PORT }};
  }

  server {
    listen 80;
    server_name backend.onthe-top.com;

    location / {
      proxy_pass http://backend;
      proxy_set_header Host \$host;
      proxy_set_header X-Real-IP \$remote_addr;
      proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
    }
  }

  server {
    listen 80;
    server_name ai.onthe-top.com;

    location / {
      proxy_pass http://ai;
      proxy_set_header Host \$host;
      proxy_set_header X-Real-IP \$remote_addr;
      proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
    }
  }
}
EOF

      - name: Transfer Nginx Config
        run: |
          scp -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} \
              nginx.conf \
              ec2-user@$TARGET_IP:/home/ec2-user/prod/nginx.conf

      - name: Reload Nginx with New Config
        run: |
          ssh -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} ec2-user@$TARGET_IP << 'EOS'
            sudo mv /home/ec2-user/prod/nginx.conf /etc/nginx/nginx.conf
            sudo nginx -t && sudo systemctl reload nginx
          EOS

Prod 배포 확정/롤백 스크립트

name: Rollback or Cleanup

on:
  workflow_dispatch:
    inputs:
      action:
        description: 'rollback or cleanup'
        required: true
        default: 'cleanup'
      target_port:
        description: '기존 포트 (예: 8080)'
        required: true
      target_id:
        description: '인스턴스 대상 (예: prod1, prod2)'
        required: true

jobs:
  control:
    name: Control ${{ github.event.inputs.target_id }}
    runs-on: ubuntu-latest

    env:
      BASE_BACKEND_PORT: 8080
      BASE_AI_PORT: 8000

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Map IP
        run: |
          if [ "${{ github.event.inputs.target_id }}" = "prod1" ]; then
            echo "TARGET_IP=${{ secrets.PROD_INSTANCE_1 }}" >> $GITHUB_ENV
          elif [ "${{ github.event.inputs.target_id }}" = "prod2" ]; then
            echo "TARGET_IP=${{ secrets.PROD_INSTANCE_2 }}" >> $GITHUB_ENV
          else
            echo "Unknown target_id" && exit 1
          fi

      - name: Perform Action
        run: |
          ACTION="${{ github.event.inputs.action }}"
          PORT=${{ github.event.inputs.target_port }}

          ssh -o ProxyJump=ec2-user@${{ secrets.JUMP_HOST }} ec2-user@$TARGET_IP << EOS

            if [ "$ACTION" = "rollback" ]; then
              echo "🔄 롤백 수행 중: 포트 $PORT 로 Nginx 설정 전환"

              sudo sed -i "s/127.0.0.1:[0-9]\{4\}/127.0.0.1:$PORT/g" /etc/nginx/nginx.conf
              sudo nginx -t && sudo systemctl reload nginx

              echo "✅ 롤백 완료"
            elif [ "$ACTION" = "cleanup" ]; then
              echo "🧹 이전 컨테이너 종료 및 제거 (포트: $PORT)"

              docker ps | grep ":$PORT->" | awk '{print \$1}' | xargs -r docker stop
              docker ps -a | grep ":$PORT->" | awk '{print \$1}' | xargs -r docker rm

              echo "✅ 정리 완료"
            else
              echo "⚠️ 잘못된 action 입력: $ACTION"
              exit 1
            fi

          EOS

기술 구성 명세

본 프로젝트는 Docker 기반의 Blue-Green 배포 전략을 중심으로, 클라우드 네이티브한 아키텍처를 Dev 환경부터 먼저 적용한 후, 점진적으로 EKS로 전환할 계획입니다. 이 섹션은 현재 Dev 환경 기준의 구성을 설명합니다.

컨테이너 및 이미지 관리

  • Docker: Spring Boot, FastAPI, Frontend 등 모든 서비스는 Dockerfile을 기반으로 컨테이너화되어 있으며, GitHub Actions를 통해 자동 빌드됩니다.
  • Docker Compose: Dev 환경에서는 docker-compose를 사용하여 애플리케이션을 실행합니다.
  • DockerHub: 이미지들은 prod, dev, latest 등의 태그로 관리되며, prod 배포를 위해 버전 태그를 병행합니다.

CI/CD 파이프라인

  • GitHub Actions:
    • main 브랜치는 prod 배포, dev 브랜치는 dev 배포로 연결됩니다.
    • Dev의 경우 Backend, AI, Frontend 각각 독립된 파이프라인을 구성하여 서비스별 개별 배포가 가능합니다.
    • Prod의 경우 Matrix 전략을 이용해 여러 서버에 병렬 배포를 수행하며, 포트 단위로 라우팅 전환됩니다.
  • Secrets/Environment 관리:
    • CI 빌드 단계에서는 GitHub Secrets를 사용하고, 런타임에서는 Secret Manager로부터 값을 주입합니다.

네트워크 및 배포 전략

  • Nginx:
    • 단일 VM에서 여러 컨테이너를 운영하며, 포트 기반 라우팅으로 Blue-Green 전환을 수행합니다.
  • GCP HTTPS Load Balancer:
    • 공인 엔드포인트에서 SSL 종료 및 트래픽 분산을 담당하며, 내부적으로는 Nginx를 거쳐 서비스별 포트로 라우팅됩니다.

보안 및 비밀 관리

  • GCP Secret Manager:
    • MySQL 계정, 외부 API 키 등을 저장하고, .env 파일 형태로 컨테이너에 주입됩니다.
  • GitHub Secrets:
    • DockerHub 인증 정보, 배포 도메인 정보 등 CI 관련 민감 데이터를 관리합니다.
  • 향후 계획:
    • EKS 도입 및 AWS 마이그레이션 시 AWS Secrets Manager 기반 구성으로의 이전을 고려하고 있습니다.

모니터링 및 운영

  • Prometheus, Grafana, Alertmanager:
    • node-exporter, blackbox-exporter 등을 활용하여 인프라 및 서비스 상태를 모니터링합니다.
    • 알람은 Discord로 연동 예정입니다.
  • 로그 수집:
    • stdout 로그는 Docker 로그로 수집되며 이후 로그 시스템 도입을 검토 중입니다.

데이터베이스

  • MySQL:
    • 현재 GCE 인스턴스 기반으로 운영되고 있으며, 컨테이너 외부에서 상태 유지형으로 구성되어 있습니다.

향후 계획

  • Kubeadm을 통한 테스트 클러스터 구성 후, 점진적으로 EKS 환경으로 이전할 계획입니다.
  • Helm 또는 Kustomize를 도입하여 EKS 환경의 배포 자동화를 구성할 예정입니다.
  • Secret 관리 및 환경변수 주입은 AWS 환경으로 이전하면서 AWS Secrets Manager 연동을 고려하고 있습니다.

사용 기술 스택 요약

구성 요소 기술 도구
컨테이너화 Docker, Docker Compose
이미지 저장소 DockerHub
CI/CD GitHub Actions
배포 전략 Blue-Green, Nginx 포트 전환
비밀 관리 GCP Secret Manager, GitHub Secrets
로깅/모니터링 Prometheus, Grafana, Alertmanager
데이터베이스 MySQL (GCE 기반)
인프라 플랫폼 GCP VM, GCP Load Balancer
향후 전환 계획 Kubeadm → EKS, Helm/Kustomize 예정
⚠️ **GitHub.com Fallback** ⚠️