ESPNWeek 1 overreactions: LSU is back! Michigan has issues! The ACC is a blast!ESPN DeportesAroldis Chapman logra su salvamento 400The Jerusalem PostIAEA has been completely blind to Iran’s nuclear program for over a year, Rafael Grossi saysוואלהשני אקדחי 'קרלו' ומחסניות אותרו בשטח ליד כפר קאסםDaily MaverickFINDING PURPOSE: Boks drew on deep well of resolve and perspective to fuel another victorious momentRTP Desporto12h30 Madjer pede FC Porto sem medo frente ao CityBollywood HungamaEXCLUSIVE: Sagar Pictures Entertainment to unveil first teaser poster of Shrimad Bhagavatam on September 10SCMP ChinaChina vows deeper cooperation with Qatar in energy, AI and Middle East de-escalationScreen RantAnother Streaming Service Can Finally Dethrone Apple TV As The King Of Sci-FiObservador DesportoConselheiro por Cabo Verde pede ajuda à Cruz VermelhaMalay MailLee Chong Wei Cup to go international next year, seeks Badminton Asia sanctionPopular ScienceNearly 500 animals rescued from trafficking arrive at a California aquarium
The Daily Newsstand · Free, Always
Monday, September 7, 2026

Zero Downtime Deploy: как обновлять production, не роняя пользователей

Translate

Представим ситуацию: у вас есть примитивное веб‑приложение онлайн‑редактора, которое уже набрало среднюю нагрузку под 100 RPS. И вы, как тимлид, дождались того самого дня, когда вся кропотливая работа над обновлением и бессонные ночи наконец завершились. Настал момент релиза. Вы радуетесь, что пользователи вашего приложения станут ещё счастливее. Они доверяют вашему сервису свои важные задачи, а вы расширите их возможности и сделаете их жизнь ещё немного лучше. И, вот момент:

3, 2, 1…

Релиз состоялся. Вы рады. Ваши коллеги тоже рады. Всё прошло успешно. Но спустя некоторое время к вам заходит начальство с максимально неприятным выражением лица: — Вы что натворили? Несколько наших пользователей потеряли свою работу, потому что не успели её сохранить. Вы сначала не понимаете, что произошло. Как? Почему? На самом деле всё максимально просто. Во время деплоя ваши пользователи попали в момент простоя сервера. Приложение на некоторое время перестало обрабатывать запросы, и часть данных была потеряна. Благо, надеюсь, эта статья поможет многим не совершать подобных ошибок.

Дальше мы подробно разберём, почему вообще возникает downtime во время деплоя, какие проблемы он может создать для пользователей и главное — как организовать релиз так, чтобы обновлять приложение без остановки сервиса. Давайте для начала разберёмся, что именно произошло во время такого деплоя. Допустим, наше приложение сейчас работает на версии v1.0. Пользователи отправляют в него запросы, сервер их обрабатывает, всё хорошо. Приходит время обновления. Мы собираем новую версию приложения — v2.0 — и хотим заменить ей старую.

Так давайте же рассмотрим, как организовать релиз так, чтобы обновлять приложение без остановки сервиса. Для этого соберём небольшое приложение на FastAPI и специально воспроизведём ситуацию, при которой во время обычного деплоя пользователи на несколько секунд теряют доступ к сервису. А затем попробуем решить эту проблему с помощью нескольких экземпляров приложения и Nginx.

app/main.py

import os

from fastapi import FastAPI

app = FastAPI()

VERSION = os.getenv("VERSION", "0.1.0")

@app.get("/")
def root():
    return {
        "version": VERSION,
        "message": "Hello from Application!"
        }

@app.get("/health")
def health():
    return {
        "status": "ok"
    }

Здесь специально всё примитивно. VERSION берём из переменной окружения. Благодаря этому мы сможем запустить один и тот же код как 1.0.0, а потом как 2.0.0.

Dockerfile

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app ./app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Теперь собираем:

docker build -t zero-downtime-demo:1.0.0 .

Запускаем:

docker run -d \
  --name app \
  -p 8000:8000 \
  -e VERSION=1.0.0 \
  zero-downtime-demo:1.0.0

Проверка:

curl http://localhost:8000/

Мы должны получить вот такой ответ:

{
  "message": "Hello from application",
  "version": "1.0.0"
}

Имитируем обычный деплой

Теперь самое интересное — специально сделаем всё неправильно. Представим, что вышла новая версия приложения:

Соберём новый образ

docker build -t zero-downtime-demo:2.0.0 .

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

docker stop app
docker rm app

После этого запускаем новую версию:

docker run -d \
  --name app \
  -p 8000:8000 \
  -e VERSION=2.0.0 \
  zero-downtime-demo:2.0.0

На первый взгляд всё нормально. Но между этими двумя командами приложение вообще не работает! И вот это окно нам сейчас нужно поймать.

Показываем downtime

Чтобы не проверять приложение вручную, сделаем небольшой цикл запросов. В отдельном терминале выполняем:

while true; do
  curl -s http://localhost:8000/
  echo
  sleep 0.2
done

Теперь перезапускаем контейнер:

docker stop app
docker rm app

А теперь посмотрим на первый терминал:

Сервер находится в downtime

Сервер находится в downtime

Как мы видим, цикл запросов пошел, и терминал пуст. Как раз таки мы попали в этот период времени когда он недоступен(downtime).

И дальше не спешим.

Следующим шагом мы сделаем небольшую задержку при старте приложения, чтобы downtime было ещё нагляднее, а потом поставим перед двумя версиями Nginx. Получится:

Downtime-Деплой схема

Downtime‑Деплой схема

Что же нам нужно изменить?

На данный момент у нас всё довольно просто: пользователь напрямую обращается к одному экземпляру приложения. И главная проблема здесь очевидна: если мы остановим app:v1, пользователю просто некуда будет отправлять запросы. Значит, нам нужно добавить ещё один экземпляр приложения и поставить между пользователем и приложениями промежуточный слой, который будет решать, куда отправить запрос. Для этого воспользуемся Nginx.

Создадим docker-compose.yml:

services:
  app-v1:
    build: .
    environment:
      VERSION: 1.0.0

  app-v2:
    build: .
    environment:
      VERSION: 2.0.0

  nginx:
    image: nginx:alpine
    ports:
      - "8000:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app-v1
      - app-v2

Теперь создадим рядом файл nginx.conf:

events {}

http {
    upstream app {
        server app-v1:8000;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://app;
        }
    }
}

На данный момент Nginx отправляет все запросы только в app-v1. Запускаем:

docker compose up -d --build

И проверяем:

curl http://localhost:8000/

Получаем вот такой ответ:

{
  "message": "Hello from application",
  "version": "1.0.0"
}

А теперь обновляем приложение

Представим, что мы подготовили новую версию приложения 2.0.0. Она уже запущена, но пользователи пока о ней ничего не знают.

Проверим её напрямую:

docker compose exec nginx wget -qO- http://app-v2:8000/
{
  "message": "Hello from application",
  "version": "2.0.0"
}

То есть новая версия действительно работает. Теперь осталось переключить на неё пользовательский трафик. Меняем в nginx.conf:

upstream app {
    server app-v2:8000;
}

Но Nginx не нужно перезапускать. Выполняем:

docker compose exec nginx nginx -t

Что в итоге произошло?

Наш deployment теперь выглядит немного иначе. Именно так, как в схемах указанных выше. Сначала весь трафик идёт в app:v1. Мы запускаем app:v2 и убеждаемся, что она работает. После этого меняем конфигурацию Nginx и делаем reload. Трафик начинает идти в новую версию. И только после успешного переключения старую версию можно останавливать

docker compose stop app-v1


Подведем итоги

Надеюсь вам понравилась моя первая статья, и я помог открыть вам что‑то новое в этом безграничном мире знаний IT. Всем хорошего дня!)

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.