Docker Compose + Nginx reverse proxy로 만든 가장 단순한 Blue-Green 배포 구조

배포 자동화를 익히기 위해 작은 Flask 웹 애플리케이션 하나를 골라 Blue-Green 배포 흐름을 직접 손으로 짜본 적이 있습니다.

기능 자체보다 “어떻게 배포할 것인가”에 집중한 토이 프로젝트인데, 이 구조 자체가 Blue-Green의 핵심을 단순하게 보여주는 형태라서 정리해보려고 합니다.

이번 글에서는 다음에 집중합니다.

  • 왜 Blue-Green을 골랐는가
  • 컨테이너 / Nginx / Docker Compose가 어떻게 묶여 있는가
  • 트래픽이 한 색상에서 다른 색상으로 어떻게 넘어가는가

배포 스크립트(deploy.sh)와 Jenkins 자동화 부분은 분량이 충분히 별도라 다음 글로 분리해서 정리합니다.


1. 발단

토이 프로젝트를 여러 개 만들기보다, “작은 서비스 하나라도 배포 자동화 흐름을 구조적으로 보여주는 프로젝트”가 더 의미 있다고 느꼈습니다.

그래서 다음과 같이 목표를 잡았습니다.

  • Flask 앱을 컨테이너로 실행
  • Nginx를 reverse proxy로 두고 트래픽 라우팅 제어
  • 신규 컨테이너를 띄우는 동안 기존 컨테이너는 계속 서비스 유지
  • 헬스체크 통과 후 트래픽을 신규 쪽으로 전환
  • 이전 컨테이너는 트래픽 전환 후 종료

이 흐름을 구현하면서 자연스럽게 Blue-Green 배포의 모양이 잡혔습니다.


2. 왜 Blue-Green인가

처음에는 단순히 docker compose up으로 한 컨테이너를 재배포하는 방식부터 떠올렸습니다. 그런데 그 방식에는 다음 한계가 있었습니다.

  • 기존 컨테이너가 내려가는 짧은 시간 동안 503이 발생
  • 신규 컨테이너가 시작 직후 곧바로 트래픽을 받게 됨 → 워밍업 부족
  • 문제가 생겼을 때 즉시 롤백할 수 있는 안전망이 없음

Blue-Green 방식은 이 세 가지 한계를 가장 단순한 형태로 해결합니다.

  • 기존 컨테이너(예: blue)는 트래픽을 그대로 받고 있는 채로
  • 신규 컨테이너(green)를 먼저 띄우고
  • 헬스체크가 통과되면 Nginx의 proxy_pass 대상을 green으로 바꾸고
  • 그 시점 이후로 기존(blue)을 종료

즉, 사용자 진입점(Nginx)은 그대로 두고, 내부 서비스 컨테이너만 교체하는 방식입니다.

이렇게 보면 Blue-Green이라는 이름은 색상 두 개를 번갈아 가리킨다는 의미일 뿐이고, 실제 본질은 “트래픽 수신 지점을 고정하고, 그 뒤의 컨테이너를 교체 가능한 자원으로 다룬다”는 점에 가깝습니다.


3. 구조

전체 구조는 3개의 컨테이너로 단순하게 잡았습니다.

[사용자]
   ↓
[Nginx (web_server)]   ← 외부 진입점, 고정
   ↓ proxy_pass
[draw_blue]  또는  [draw_green]   ← 둘 중 하나만 활성

각 컨테이너 역할은 다음과 같습니다.

컨테이너 역할
web_server Nginx reverse proxy. 외부 80 포트 진입을 받아 내부 컨테이너로 전달
draw_blue Flask 앱 컨테이너. 현재 활성 또는 다음 배포 대상 중 하나
draw_green Flask 앱 컨테이너. 위와 같은 역할의 반대 색상

세 컨테이너 모두 같은 Docker 네트워크(draw_network)에 묶어두면 Nginx가 컨테이너 이름으로 내부 통신할 수 있습니다.


4. Compose 파일 분리

토이 프로젝트지만, Blue/Green 컨테이너를 서로 다른 Compose 파일로 분리한 게 핵심입니다.

random-draw-bluegreen-deploy/
├── blue.yml     # Blue 앱 컨테이너만 정의
├── green.yml    # Green 앱 컨테이너만 정의
├── nginx.yml    # Nginx 컨테이너 정의
├── deploy.sh    # 배포 스크립트
└── nginx.conf/  # Nginx 설정

이렇게 분리해 두면 다음과 같이 한 색상씩 띄우고 내릴 수 있습니다.

docker compose -f blue.yml  up -d --build
docker compose -f blue.yml  down
docker compose -f green.yml up -d --build

같은 docker-compose.yml 안에서 두 서비스를 정의해도 동작은 하지만, 파일을 나눠두면 활성 색상과 대기 색상을 명확히 분리해서 다룰 수 있다는 장점이 있습니다. 스크립트에서 “현재 어느 파일을 띄워둔 상태인가”를 단순히 컨테이너 이름으로 판단할 수 있게 됩니다.


5. Nginx reverse proxy 구성

Nginx 설정은 단순합니다. default.conf에서 proxy_pass 대상으로 현재 활성 컨테이너 이름을 잡아주면 끝입니다.

server {
    listen 80;

    location /health {
        return 200 'ok';
    }

    location / {
        proxy_pass         http://draw_blue:5179;
        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_pass http://draw_blue:5179 부분을 proxy_pass http://draw_green:5179로 교체하는 게 트래픽 전환의 핵심입니다.

교체 후 Nginx만 재시작하면 됩니다.

docker exec web_server nginx -s reload

reload는 기존 연결을 유지한 채 새 설정을 적용하기 때문에, 이 시점에 들어오는 신규 요청부터 자연스럽게 새 컨테이너로 전달됩니다.

nginx -s reload는 graceful 한 동작이라 진행 중인 요청은 끊기지 않습니다. 컨테이너를 통째로 재기동하는 것과는 의미가 다릅니다.


6. 배포 흐름 한눈에 보기

이제 위 구조 위에서 실제 Blue-Green 흐름이 어떻게 떨어지는지 정리해 보면 이렇습니다.

1. 현재 활성 색상 확인
   → draw_blue가 떠 있다면 다음 색상은 green

2. 반대 색상 컨테이너 실행
   → docker compose -f green.yml up -d --build

3. 신규 컨테이너 헬스체크
   → curl http://draw_green:5179/health 가 200 떨어질 때까지 대기

4. Nginx 설정 교체
   → default.conf의 proxy_pass를 draw_green:5179 로 변경

5. Nginx reload
   → docker exec web_server nginx -s reload

6. 이전 컨테이너 종료
   → docker compose -f blue.yml down

이 6단계가 Blue-Green 배포의 가장 단순한 형태입니다.

이 중 4번이 진짜 트래픽 전환 시점이고, 그 직전까지는 기존 컨테이너가 정상적으로 서비스를 유지하고 있습니다.

만약 3번의 헬스체크가 실패하면, 4번 이후로 진행하지 않고 그 자리에 멈추면 됩니다. 사용자 트래픽은 여전히 기존 컨테이너가 받고 있으니, 운영 측면에서는 사실상 무중단입니다.


7. 짚어두고 싶은 것들

7-1. Blue-Green의 본질은 “트래픽 수신 지점 고정”

처음에는 “컨테이너를 두 개 띄워서 번갈아 쓰는 거”라고만 봤는데, 직접 짜보고 나니 핵심은 다른 데 있었습니다.

Nginx 같은 진입점을 하나 두고, 그 뒤의 워크로드를 교체 가능한 자원으로 다루는 것.

이 시각에서 보면 K8s Service + 두 Deployment 도 Blue-Green이고, ALB target group 교체도 Blue-Green입니다. 도구만 바뀌고 본질은 같습니다.

7-2. Compose 파일을 색상별로 나눠두면 스크립트가 단순해진다

blue.yml / green.yml 로 나눠두면, 배포 스크립트가 다음 한 줄로 끝납니다.

docker compose -f ${NEXT_COLOR}.yml up -d --build

활성 색상 판단도 단순하게 컨테이너 이름 기반으로 가능합니다.

if docker ps --format '' | grep -q draw_blue; then
  CURRENT=blue
else
  CURRENT=green
fi

같은 docker-compose.yml 안에서 두 서비스를 정의했다면 이런 단순함이 안 나옵니다.

7-3. nginx -s reload는 graceful 하다

이 명령은 다음 동작을 합니다.

  • 마스터 프로세스가 새 설정을 읽음
  • 워커 프로세스를 새 설정으로 새로 띄움
  • 기존 워커는 진행 중인 요청을 끝까지 처리한 뒤 종료

즉, 진행 중인 연결은 끊기지 않고, 새 요청만 새 설정으로 라우팅됩니다. 이 동작 덕분에 트래픽 전환 시점에 사용자 입장에서 끊김이 거의 없습니다.

7-4. 헬스체크가 진짜 안전망이다

Blue-Green이 “무중단에 가까운” 이유는 사실 헬스체크에 기댑니다.

  • 신규 컨테이너가 정상 응답할 때까지 트래픽을 안 보냄
  • 응답 못 하면 전환을 멈추고 기존 컨테이너 유지

단순 HTTP 200 만 보는 것보다는, 앱이 실제로 의존성(DB, Redis, 외부 API)에 닿을 수 있는지까지 점검하는 헬스체크가 안전합니다. 이 부분은 다음 글에서 deploy.sh 와 함께 더 다룹니다.

7-5. 토이 프로젝트로도 운영 감각이 잡힌다

기능이 단순해도, 배포 흐름을 직접 짜보면 다음 같은 감각이 잡힙니다.

  • “트래픽 수신 지점”과 “워크로드”를 분리해서 보는 사고
  • 헬스체크 통과 전후로 trust 단계를 나누는 사고
  • 롤백 가능한 단위로 배포를 쪼개는 사고

K8s나 ECS 같은 큰 도구로 옮겨가도 이 감각은 그대로 쓰입니다.


8. 마무리

Blue-Green이라는 단어를 처음 들었을 땐 “두 환경 번갈아 운영하기” 정도로만 이해했었습니다. 그런데 직접 손으로 짜보고 나니, 본질은 다음 한 줄에 가까웠습니다.

사용자 진입점은 고정하고, 그 뒤의 컨테이너는 교체 가능한 자원으로 다룬다.

이번 토이 프로젝트로 그 감각을 잡아두니, 이후에 K8s Deployment 의 rolling update 나 ALB target group 전환을 다룰 때도 같은 사고 방식을 그대로 적용할 수 있었습니다.

다음 글에서는 위에서 짧게 본 6단계 흐름을 실제로 자동화한 deploy.sh 의 내용, 즉 활성 색상 판단 / 헬스체크 / Nginx 설정 교체 / 이전 컨테이너 정리 같은 부분을 정리해보려고 합니다.


This site uses Just the Docs, a documentation theme for Jekyll.