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

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

Блог MAATRIX · 2026-07-07

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

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

['Наивный деплой — это 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/\n├── releases/\n│   ├── 20260709-1200/\n│   └── 20260709-1330/\n├── current -> releases/20260709-1330\n└── shared/  (uploads, .env, логи)
', 'Скрипт деплоя готовит новый релиз и переключает symlink одной командой ln -sfn — она заменяет ссылку атомарно, без промежуточного состояния:', '
#!/bin/bash\nset -e\nREL=/var/www/app/releases/$(date +%Y%m%d-%H%M%S)\ngit clone --depth 1 git@github.com:USER/REPO.git $REL\ncd $REL\nln -s /var/www/app/shared/.env .env\nnpm ci && npm run build\nln -sfn $REL /var/www/app/current\nsystemctl reload myapp
']

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

['Ключевое слово — reload, а не restart. gunicorn по сигналу HUP поднимает новых воркеров и мягко гасит старых, дав им завершить запросы.', '
# gunicorn: graceful reload\nkill -HUP $(cat /run/gunicorn.pid)\n# или через systemd, если ExecReload настроен на HUP\nsystemctl reload myapp
', 'В systemd-юните пропишите ExecReload и PID-файл:', '
[Service]\nExecStart=/venv/bin/gunicorn app:app --pid /run/gunicorn.pid\nExecReload=/bin/kill -HUP $MAINPID\nKillMode=mixed
']

nginx как буфер и healthcheck

['nginx перед приложением сглаживает переключение: если апстрим на миг недоступен, запрос ретраится. Добавьте retry-логику в upstream.', '
upstream app {\n    server 127.0.0.1:8000 max_fails=3 fail_timeout=5s;\n    server 127.0.0.1:8001 backup;\n}\nserver {\n    location / {\n        proxy_pass http://app;\n        proxy_next_upstream error timeout http_502;\n    }\n}
', 'Перед переключением symlink проверьте, что новый релиз реально отвечает. Простой healthcheck в скрипте деплоя:', '
for i in $(seq 1 10); do\n  curl -fs http://127.0.0.1:8000/health && break\n  sleep 1\ndone
', 'После 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\nsystemctl 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 для деплоя без простоя

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

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

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

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

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

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

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