Недавно я внедрил blue-green деплой в проде. Реализация довольно простая и кастомная, но справляется со своей задачей на ура! Также сообщу, что используется обычный докер композ на виртуалке - возможно, кому-то такой подход будет полезен.

Для фоновых процессов (воркеров)

В приложение добавляется специальный инфрастуктурный singleton класс с флагом is_accepting, и обертка на consumers. В каждом консьюмере перед обработкой проверяем этот флаг: если True - обрабатываем задачу, если False - переносим задачу на повторную обработку (например, в rabbitmq делаем сразу nack(requeue=true))

вот пример такого синглтона на ЯП python.
вот пример такого синглтона на ЯП python.
Пример обертки. Если is_accepting=False - отправляем задачи обратно в очередь.
Пример обертки. Если is_accepting=False - отправляем задачи обратно в очередь.

Когда сервис получает sigterm сигнал, этот singleton переключает is_accepting в False. После переключения добавляем ожидаение на время максимального выполнения задачи(+5-10 секунд), и в контейнере обязательно указываем graceful timeout на 5-10 секунд больше этого значения.

Переводим приложение в режим завершения(is_accepting=False), даём воркерам время закончить текущие задачи и затем корректно останавливаем остальные ресурсы. В примере используется фреймворк Faststream на rabbitmq.
Переводим приложение в режим завершения(is_accepting=False), даём воркерам время закончить текущие задачи и затем корректно останавливаем остальные ресурсы. В примере используется фреймворк Faststream на rabbitmq.

Можно сделать через хрупкий счетчик активных задач, но это лишено смысла - мы все равно упираемся в graceful timeout контейнера. Простое ожидание надежнее в этом случае.

По сути получается так: активный процесс после сигнала перестает обрабатывать новые задачи, дорабатывает текущие и корректно завершается.

Также уточню, что пример был приведен для однопоточного асинхронного воркера. Реализация может зависеть от архитектуры приложения и способа организации обработки задач. Например, если под капотом фреймворка используется Process Pool, важно учитывать, что механизм переключения состояния (через is_accepting) должен быть инициализирован в каждом дочернем процессе! Иначе консумеры в дочерних процессах будут игнорировать сигнал завершения и продолжать обработку новых задач. Возможно, что стоит подумать о распределенном ключе, который определяет активный деплой, но в нашем случае это усложнение.

В деплой скрипте логика такая: определяем активный инстанс (через регулярку/сопоставление/etc), сразу поднимаем inactive инстанс. В итоге на короткое время у нас работают оба инстанса и вместе обрабатывают задачи. Дальше отправляем sigterm активному инстансу - он переключается в режим неактивного (is_accepting=False), перестает обрабатывать новые задачи и спокойно дожидается завершения текущих.

Вот пример скрипта деплоя:

set -e

DEPLOY_PATH="$1"
COMPOSE="docker compose -f ${DEPLOY_PATH}/docker-compose.prod.yml"

if docker ps --format '{{.Names}}' | grep -q "^notifier-blue$"; then
  ACTIVE="blue"
  INACTIVE="green"
else
  ACTIVE="green"
  INACTIVE="blue"
fi

echo "[notifier] active=${ACTIVE}, deploying to=${INACTIVE}"

$COMPOSE up -d notifier-${INACTIVE}

$COMPOSE stop notifier-${ACTIVE}

echo "[notifier] done"

P.S. Для идемпотентныхконсьюмеров все еще проще — можно почти ничего не делать =) достаточно рейзить специальную ошибку с requeue=True, и условный RabbitMQ сам отправит сообщение обратно в очередь. Но проблема возникает с заполнением очереди, т.к во время деплоя мы не будем обрабытвать сообщения. С неидемпотентными сообщениями такой подход уже проблемный — при повторной обработке мы получим неконсистентное состояние(например, упадем при проверке id сообщения). А неидемпотентных сообщений, как правило, большинство.

Для веб сервисов

В конфиге у нас всегда есть 2 инстанса приложения — blue и green. Также нужен реверс‑прокси, например nginx.

Логика переключения реализована в деплой скрипте. Сначала определяем, какой инстанс сейчас активный. Это можно сделать через if логику: если активен blue - Active = blue, Inactive = green, в любом другом случае - наоборот Active=green, Inactive = blue. Определить это можно по регулярке/активному порту/etc. После этого запускаем inactive инстанс и проверяем его через healthcheck.

Дальше переключаем nginx на новый инстанс, что-то типа: echo “server ${HOST}${INACTIVE_PORT};” > “$UPSTREAM_CONF” и делаем мягкий бесшовный reload(nginx -s reload). После этого устанавливаем время ожидания, равное максимальному времени выполнения http запроса в вашем сервисе(с запасом). Затем старому инстансу посылаем sigterm сигнал.

Вот пример скрипта деплоя:

set -e

IMAGE="$1"
DEPLOY_PATH="$2"

BLUE_PORT=${BLUE_PORT:-8002}
GREEN_PORT=${GREEN_PORT:-8003}

UPSTREAM_CONF="/etc/nginx/snippets/web-upstream.conf"
COMPOSE="docker compose -f ${DEPLOY_PATH}/docker-compose.prod.yml"

if grep -q "${BLUE_PORT}" "$UPSTREAM_CONF"; then
  ACTIVE="blue"
  INACTIVE="green"
  INACTIVE_PORT=$GREEN_PORT
else
  ACTIVE="green"
  INACTIVE="blue"
  INACTIVE_PORT=$BLUE_PORT
fi

echo "[blue-green] active=${ACTIVE}, deploying to=${INACTIVE} (port ${INACTIVE_PORT})"

$COMPOSE up -d web-${INACTIVE}

echo "[blue-green] waiting for health..."
for i in $(seq 1 30); do
  if curl -sf "http://127.0.0.1:${INACTIVE_PORT}/v1/health" > /dev/null 2>&1; then
    echo "[blue-green] healthy after ${i} attempts"
    break
  fi
  if [ "$i" -eq 30 ]; then
    echo "[blue-green] health check failed, rolling back"
    $COMPOSE stop web-${INACTIVE}
    exit 1
  fi
  sleep 2
done

echo "server 127.0.0.1:${INACTIVE_PORT};" > "$UPSTREAM_CONF"
nginx -t && nginx -s reload
echo "[blue-green] nginx switched to ${INACTIVE}"

sleep 5

$COMPOSE stop web-${ACTIVE}
echo "[blue-green] stopped web-${ACTIVE}, deploy complete"

Немного о миграциях

В blue green деплое важно, чтобы новая и старая версии приложения могли одновременно работать с одной схемой бд. Поэтому используем backward compatible подход: Например, нам нужно удалить атрибут или целую таблицу - сначала убираем её использование в коде и делаем деплой. После переключения трафика и завершения работы старых инстансов выполняем второй деплой с миграциями на удаление. Да, больше работы - но это того стоит.

Пример конфигурации Docker Compose

Вот фрагмент докер композ файла:

services:
  ...

  web-blue:
    image: ${CI_REGISTRY_IMAGE}/web:${IMAGE_TAG:-latest}
    container_name: web-blue
    restart: always
    env_file:
      - .env.prod
    ports:
      - "127.0.0.1:8002:8000"
    depends_on:
      migrations:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy

  web-green:
    image: ${CI_REGISTRY_IMAGE}/web:${IMAGE_TAG:-latest}
    container_name: web-green
    restart: always
    env_file:
      - .env.prod
    ports:
      - "127.0.0.1:8003:8000"
    depends_on:
      migrations:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy

  worker-blue:
    image: ${CI_REGISTRY_IMAGE}/worker:${IMAGE_TAG:-latest}
    container_name: worker-blue
    restart: always
    stop_grace_period: 200s
    env_file:
      - .env.prod
    depends_on:
      migrations:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      redis:
        condition: service_healthy

  worker-green:
    image: ${CI_REGISTRY_IMAGE}/worker:${IMAGE_TAG:-latest}
    container_name: worker-green
    restart: always
    stop_grace_period: 200s
    env_file:
      - .env.prod
    depends_on:
      migrations:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      redis:
        condition: service_healthy

  notifier-blue:
    image: ${CI_REGISTRY_IMAGE}/notifier:${IMAGE_TAG:-latest}
    container_name: notifier-blue
    restart: always
    stop_grace_period: 15s
    env_file:
      - .env.prod
    depends_on:
      migrations:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      redis:
        condition: service_healthy

  notifier-green:
    image: ${CI_REGISTRY_IMAGE}/notifier:${IMAGE_TAG:-latest}
    container_name: notifier-green
    restart: always
    stop_grace_period: 15s
    env_file:
      - .env.prod
    depends_on:
      migrations:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      redis:
        condition: service_healthy

  ...

Заключение

В итоге получился вполне рабочий blue‑green деплой с нулевым даунтаймом, и всё это на обычном докер композ.

Если есть идеи, как сделать проще и надежнее, или замечания по подводным камням, которые я мог не учесть — делитесь, буду рад почитать!

Комментарии (8)


  1. baldr
    20.04.2026 19:16

    Как я понимаю, продакшен у вас - это один сервер? Даже в таком случае можно включить Docker Swarm на сервере и тогда вам будет доступен deploy режим с его фичами по плавному апдейту и роллбеку. Есть и минусы - вы сразу теряете depends_on.


    1. merra123 Автор
      20.04.2026 19:16

      Да, на одном сервере. Спасибо за ссылку, посмотрю


  1. zartdinov
    20.04.2026 19:16

    Может полезно будет docker compose --profile green ...
    И темплейты возможно, не сильно ими пользовался


    1. merra123 Автор
      20.04.2026 19:16

      не использовал профили. спасибо, посмотрю


  1. pashuk
    20.04.2026 19:16

    K3s поставьте поэкспериментируйте, там совсем другой уровень.

    Там хоть сине зеленое, хоть канареечное развертывание можно сделать просто меняя labels у service.

    Расписывать не буду, инфы море. Но для старта ваш проект хороший конечно


    1. merra123 Автор
      20.04.2026 19:16

      Спасибо, посмотрю на k3s. Надеюсь 4гб оперативки на сервере хватит)


  1. propell-ant
    20.04.2026 19:16

    получился вполне рабочий blue‑green деплой с нулевым даунтаймом

    Но есть побочка: принудительный NACK посреди обработки может порождать непредвиденые и нетестированные состояния системы.


    1. merra123 Автор
      20.04.2026 19:16

      мы не прерываем уже выполняющиеся задачи, мы просто не принимаем новые, т.е отправляем их обратно в очередь на обработку нашему новому активному фоновому процессу