클라우드 4단계: Docker 컨테이너화 배포 - 100-hours-a-week/16-Hot6-wiki GitHub Wiki
상위 문서: 클라우드 위키
관련 문서: 클라우드 WHY? 문서
이 문서는 기존 인스턴스 로컬 실행 기반 서비스 배포 구조를 개선하기 위해 Docker를 도입한 과정을 정리한 기술 문서입니다.
환경별 개발 편차, 수동 배포의 반복, 운영 환경과 로컬 환경 간의 불일치 등 다양한 문제를 해결하고자
컨테이너 기반의 일관된 실행 환경과 자동화된 배포 구조를 구축하였습니다.
Docker 도입 이후, Dev/Prod 환경을 분리하고 GitHub Actions 기반 CI/CD를 구성했으며,
Blue-Green 방식의 포트 기반 배포 전략을 통해 롤백 가능성과 서비스 무중단 배포를 확보하였습니다.
본 문서에서는 Docker 도입 배경과 그 선택 기준부터 시작하여,
컨테이너 구조 설계, 이미지 태그 전략, 환경 변수 관리 방식,
Dev/Prod 배포 자동화 워크플로우, Blue-Green 배포 및 롤백 구조까지 전체 흐름을 단계별로 설명합니다.
앞으로의 EKS 전환 계획과 함께, 이 컨테이너화 전략은 팀의 운영 효율성과 서비스 확장성을 동시에 고려한 핵심 인프라로 작동하게 될 것입니다.
- 🔁 Blue-Green 배포에서 인스턴스 전체를 교체하지 않고 포트 단위로 컨테이너를 전환할 수 있게 되었음. 이 구조는 기존 인프라 기반의 배포보다 훨씬 빠르고 안전하며, 롤백 또한 컨테이너 단위로 손쉽게 수행할 수 있음
- 이 구조적 이점은 Terraform 기반의 인프라 관리와도 충돌 없이 조화롭게 작동하며, CD 자동화의 핵심 기반이 됨.
- 개발자마다 환경이 달라 생기던 문제(Python 버전, 경로 불일치 등)를 Docker가 해결해 줌
- 운영 환경과 동일한 컨테이너 기반으로 로컬 테스트 가능 → 재현성과 안정성 확보
- 특히 Python 기반 FastAPI는 의존성 복잡도가 높아 Docker와 궁합이 뛰어났음
- GitHub Actions, 로컬, 서버 등 전 구간에서 동일 환경을 보장하는 기준점 역할
Docker는 단순한 실행 도구가 아니라, 우리 팀의 CD 전략을 완성하고 개발-배포 체계를 안정화하는 핵심 인프라였습니다.
-
docker run
에 비해 구조와 실행 방식을 쉽게 공유 가능 → 처음 보는 개발자도 이해 쉬움 -
.env
와Secret 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는 단순 보안 도구가 아닌, 인프라 수준에서 신뢰 가능한 보안 구성 요소였습니다.
왜 prod에서 HTTPS LB와 Nginx를 동시에 사용하나요?
컨테이너 이미지 태그는 버전 관리, 배포 트리거, 롤백 가능성 등을 좌우하는 핵심 전략입니다.
저희는 다음과 같은 기준으로 태그를 설계했습니다.
이미지 | 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
)
-
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-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 환경은 다음과 같은 흐름으로 배포됩니다:
- 프론트엔드: S3 + CloudFront
- 백엔드/AI: Jump Server → Target Server로 SSH, 이후
docker-compose
실행
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 "/*"
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
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 환경의 경우 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:
정확한 배포 플로우는 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
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 배포를 위해 버전 태그를 병행합니다.
- 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
파일 형태로 컨테이너에 주입됩니다.
- MySQL 계정, 외부 API 키 등을 저장하고,
- 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 예정 |