MAATRIX / Блог / Zero-downtime деплой на VPS без простоя

Zero-downtime деплой на VPS без простоя

Zero-downtime деплой на VPS: обновление без простоя
Блог MAATRIX · 2026-07-07

Пользователь не должен видеть 502 во время выката. Разберём проверенную схему деплоя без простоя: релизы через symlink, graceful reload приложения и проверка здоровья перед переключением трафика.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Почему возникает даунтайм

Наивный деплой — это git pull поверх работающего кода и рестарт сервиса. В момент рестарта старый процесс убит, новый ещё не принимает соединения — отсюда 502 и оборванные запросы.

Zero-downtime строится на двух принципах:

  • Атомарное переключение — новый релиз готовится в стороне, трафик переводится мгновенной сменой symlink.
  • Graceful reload — новый процесс поднимается рядом со старым, а старый завершает текущие запросы и только потом уходит.

Быстрый NVMe у MAATRIX сокращает время подготовки релиза (распаковка, установка зависимостей), а значит окно риска между сборкой и переключением минимально. Чем быстрее готовится новый релиз, тем короче период, когда на диске лежат две версии и что-то может пойти не так.

Отдельно стоит понимать разницу между zero-downtime и high-availability. Zero-downtime гарантирует, что обновление не роняет сервис. HA защищает от отказа железа. Схема ниже решает первую задачу на одном сервере; для второй понадобится второй узел и балансировщик перед ними.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США и РФ. Оплата картой РФ и по СБП.

Арендовать VPS для деплоя без простоя

Релизы через symlink

Каждый деплой кладём в отдельный каталог по времени, а current — это symlink на активный релиз. Переключение атомарно.

/var/www/app/
├── releases/
│   ├── 20260709-1200/
│   └── 20260709-1330/
├── current -> releases/20260709-1330
└── shared/  (uploads, .env, логи)

Скрипт деплоя готовит новый релиз и переключает symlink одной командой ln -sfn — она заменяет ссылку атомарно, без промежуточного состояния:

#!/bin/bash
set -e
REL=/var/www/app/releases/$(date +%Y%m%d-%H%M%S)
git clone --depth 1 git@github.com:USER/REPO.git $REL
cd $REL
ln -s /var/www/app/shared/.env .env
npm ci && npm run build
ln -sfn $REL /var/www/app/current
systemctl reload myapp

Graceful reload приложения

Ключевое слово — reload, а не restart. gunicorn по сигналу HUP поднимает новых воркеров и мягко гасит старых, дав им завершить запросы.

# gunicorn: graceful reload
kill -HUP $(cat /run/gunicorn.pid)
# или через systemd, если ExecReload настроен на HUP
systemctl reload myapp

В systemd-юните пропишите ExecReload и PID-файл:

[Service]
ExecStart=/venv/bin/gunicorn app:app --pid /run/gunicorn.pid
ExecReload=/bin/kill -HUP $MAINPID
KillMode=mixed

nginx как буфер и healthcheck

nginx перед приложением сглаживает переключение: если апстрим на миг недоступен, запрос ретраится. Добавьте retry-логику в upstream.

upstream app {
    server 127.0.0.1:8000 max_fails=3 fail_timeout=5s;
    server 127.0.0.1:8001 backup;
}
server {
    location / {
        proxy_pass http://app;
        proxy_next_upstream error timeout http_502;
    }
}

Перед переключением symlink проверьте, что новый релиз реально отвечает. Простой healthcheck в скрипте деплоя:

for i in $(seq 1 10); do
  curl -fs http://127.0.0.1:8000/health && break
  sleep 1
done

После nginx -t и systemctl reload nginx конфиг применяется без разрыва соединений. Всегда прогоняйте nginx -t перед reload: битый конфиг на reload не применится, и вы останетесь на старом рабочем — но узнать об ошибке лучше до, чем во время выката.

Резервный (backup) апстрим на втором порту — дешёвая подстраховка: пока основной процесс перезапускается, nginx на миг уводит трафик на запасной. Для критичных сервисов этот запасной воркер держат постоянно поднятым.

Откат и очистка

Симлинк-схема даёт мгновенный откат: переключите current на предыдущий релиз и сделайте reload.

ln -sfn /var/www/app/releases/20260709-1200 /var/www/app/current
systemctl reload myapp

Старые релизы удаляйте, оставляя последние 5, чтобы не забить диск:

cd /var/www/app/releases && ls -1t | tail -n +6 | xargs rm -rf

Частые ошибки

  • restart вместо reload — процесс убивается жёстко, запросы рвутся.
  • cp вместо ln -sfn — копирование не атомарно, ловите полурелиз.
  • Нет healthcheck — переключились на битый релиз и уронили прод.
  • Миграции БД — несовместимые схемы ломают старые воркеры. Делайте обратно-совместимые миграции в два шага.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США и РФ. Оплата картой РФ и по СБП.

Арендовать VPS для деплоя без простоя

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Частые вопросы

Zero-downtime возможен на одном сервере?

Да. symlink-релизы и graceful reload не требуют второго сервера — всё работает на одном VPS.

А если меняется схема БД?

Используйте expand/contract: сначала добавляете новые поля совместимо, деплоите код, затем отдельным шагом удаляете старое.

Reload точно не рвёт соединения?

gunicorn с graceful reload дожидается завершения текущих запросов у старых воркеров. Настройте graceful_timeout под ваши долгие запросы.