Notice
Recent Posts
Recent Comments
Link
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

빠르게 학습하고 빠르게 적용하자

홈서버 Blue-Green 무중단 배포 구축하기 본문

카테고리 없음

홈서버 Blue-Green 무중단 배포 구축하기

osoohynn 2026. 5. 1. 15:49

무중단 배포 배경

빠른 실험 전략을 선호하던 저는 Querify 서비스를 운영하며 여러 번 과감한 시도를 했습니다. Github Branch 전략을 사용하고 있었기 때문에 개발할 때는 배포 환경에서 이슈가 발생하면 빠르게 main브랜치에 올리는 방식을 이용했었습니다.

그러나 사용자가 조금씩 생기다 보니.. 배포 환경에서의 문제를 사용자가 겪는 상황이 있었고, 이 상황에서 사용할 롤백 장치가 마련되어있지 않아 곤란했습니다. 또한 새로운 기능을 추가하거나 수정하는 배포 과정에서도 서버가 일시적으로 다운되기 때문에 사용자 경험이 많이 떨어졌습니다.

그래서 기존에 알고 있던 무중단 배포 전략을 활용하여 배포할때도 서버가 다운되지 않고, 급한 이슈를 롤백으로 해결할 수 있는 환경을 마련하고자 하였습니다.

 

배포 전략 선택

블루 그린 방식은 리소스 두 배라는 약점이 있습니다. 하지만 홈서버에서 구축되어 있었기 때문에 클라우드 비용도 없고 남는 메모리로 충분히 감당이 되었습니다.

반대로 장점은 실험 단계에서는 실수가 잦은데, 라우팅만 수정하면 롤백되어서 빠르게 이슈를 처리하기 좋습니다. Compose + Nginx upstream 전환이면 쉽게 구현할 수도 있습니다.

마지막으로 확장성을 고려했습니다. MSA로 쪼개면 서비스마다 독립적으로 Blue-Green을 돌릴 수 있습니다.

            [ Nginx ]
                ↓
[ Blue (8082) ] or [ Green (8083) ]

 

블루 그린 중에서도 낮은 복잡도와 빠른 롤백의 장점을 극대화하기 위해 하나의 인스턴스(홈서버 한 대)에서 Docker compose로 앱 컨테이너를 관리하는 방식을 채택했습니다.

 

롤링 업데이트 방식은 교체 중에 v1과 v2가 같은 트래픽을 나눠 받는 구간이 있어서 API 호환성 문제가 생기기 쉽습니다. 롤백도 번거로워서 빠르게 롤백이 필요한 상황에 맞지 않아서 배제하였습니다.

Canary는 새 버전을 일부 트래픽에만 노출시키는 방법인데, 실제 사용자 트래픽으로 검증할 수 있다는 게 핵심 장점입니다. 그러나 모니터링을 꼼꼼하게 붙혀야하며, 롤링 방식과 마찬가지로 버전 관리가 필요하여 배제하였습니다.

 

구현

우선 기존 docker-compose.yml를 app과 분리하여 docker-compose.infra.yml를 새로 작성했습니다.

# docker-compose.infra.yml
services:
  mysql:
    image: mysql:8.0
    container_name: soj-mysql
    # ... 기존 그대로
    networks: [soj-network]

  redis:
    image: redis:7-alpine
    container_name: soj-redis
    # ...
    networks: [soj-network]

  mongodb:
    image: mongo:4.4
    container_name: soj-mongodb
    # ...
    networks: [soj-network]

  kafka:
    image: apache/kafka:3.7.0
    container_name: soj-kafka
    # ...
    networks: [soj-network]

  # ... ES, Logstash, Kibana, mysql-sandbox 등

networks:
  soj-network:
    name: soj-network
    driver: bridge

volumes:
  mysql_data:
  redis_data:
  mongo_data:
  es_data:
  kafka_data:

docker-compose.blue.yml과 docker-compose.green.yml 두 파일로 나누었습니다.

# docker-compose.blue.yml
services:
  app-blue:
    image: ${APP_IMAGE:-soj-backend:latest}
    container_name: soj-app-blue
    env_file: .env
    expose:
      - "8080"
    networks:
      - soj-network
    healthcheck:
      test: ["CMD", "curl", "-f", "<http://localhost:8080/actuator/health>"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 30s
    restart: unless-stopped

networks:
  soj-network:
    external: true
# docker-compose.green.yml
services:
  app-green:
    image: ${APP_IMAGE:-soj-backend:latest}
    container_name: soj-app-green
    env_file: .env
    expose:
      - "8080"
    networks:
      - soj-network
    healthcheck:
      test: ["CMD", "curl", "-f", "<http://localhost:8080/actuator/health>"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 30s
    restart: unless-stopped

networks:
  soj-network:
    external: true

Nginx upstream을 이용하도록 수정하였습니다!

server {
    server_name api.querify.dev;
    location / {
        proxy_pass http://soj_backend;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
    }
    listen [::]:443 ssl;
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/api.querify.dev/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.querify.dev/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
    if ($host = api.querify.dev) {
        return 301 https://$host$request_uri;
    }
    listen 80;
    listen [::]:80;
    server_name api.querify.dev;
    return 404;
}
upstream soj_backend {
    server 127.0.0.1:8082;
    keepalive 32;
}

deploy.sh파일로 배포 자동화를 구현하였습니다.

#!/bin/bash
# 무중단 배포 스크립트
set -euo pipefail

cd "$(dirname "$0")"

NEW_IMAGE="${1:-soj-backend:latest}"
UPSTREAM_FILE="/etc/nginx/conf.d/soj-upstream.conf"

# 색상 출력
log()   { echo -e "\\033[0;32m[$(date +%H:%M:%S)]\\033[0m $1"; }
warn()  { echo -e "\\033[1;33m[$(date +%H:%M:%S)] WARN:\\033[0m $1"; }
error() { echo -e "\\033[0;31m[$(date +%H:%M:%S)] ERROR:\\033[0m $1"; exit 1; }

# === 1. 현재 색깔 확인 ===
[ -f .current_color ] || echo "blue" > .current_color
CURRENT=$(cat .current_color)

if [ "$CURRENT" = "blue" ]; then
    NEW="green"
    NEW_PORT="8083"
else
    NEW="blue"
    NEW_PORT="8082"
fi

log "배포 시작: $CURRENT → $NEW (port $NEW_PORT, image: $NEW_IMAGE)"

# === 2. 새 컨테이너 띄우기 ===
log "$NEW 컨테이너 시작..."
export APP_IMAGE="$NEW_IMAGE"
docker compose -f docker-compose.$NEW.yml up -d

.
.
.

# === 6. 외부 검증 ===
sleep 3
EXTERNAL=$(curl -sf -o /dev/null -w "%{http_code}" <https://api.querify.dev/actuator/health> || echo "000")
if [ "$EXTERNAL" != "200" ]; then
    warn "외부 응답 비정상 ($EXTERNAL), 롤백 검토 필요"
fi

# === 7. 드레이닝 ===
log "$CURRENT 드레이닝 (15초 대기)..."
sleep 15

# === 8. 구버전 종료 ===
docker compose -f docker-compose.$CURRENT.yml down
echo "$NEW" > .current_color
log "✅ 배포 완료: $CURRENT → $NEW ($NEW_IMAGE)"

 

후기

무중단 배포를 구현할 때는 여러 개의 인스턴스를 돌려야만 하는 줄 알았는데, Docker Compose와 Nginx만으로도 구현할 수 있어서 간편했습니다. 배포시 다운타임을 없애는 것과 롤백할 수 있는 환경을 모두 만족했기 때문에 현재는 이 정도로 충분하고 나중에 트래픽이 커지거나 가용성이 진짜로 필요해지는 시점에 같은 Blue-Green 패턴을 로드 밸런스와 여러 대의 서버로 옮기고자 합니다.

다른 관점이나 잘못된 판단이 보이면 댓글 부탁드립니다!!