Как делать11 октября 2026 г.8 мин чтения
Docker и деплойШаг 7 из 8

Деплой: VPS, PaaS или контейнеры и выкатка без простоя

Коротко

VPS даёт полный контроль над сервером, но обновления, HTTPS, мониторинг и выкатка без простоя — на вас; PaaS вроде Vercel, Render или Fly.io берёт это на себя ценой рамок платформы; Kubernetes нужен, когда сервисов и экземпляров много и нужна автоматическая оркестрация. Выкатка без простоя в любой модели строится одинаково: новая версия поднимается рядом со старой, проходит health check, получает трафик, а старая мягко завершает текущие запросы. Миграции БД при этом делают обратно совместимыми, чтобы откат кода оставался возможным.

СравнениеУровень: СреднийДеплой

После материала вы сможете

  • Сравнить VPS, PaaS и контейнерную оркестрацию по контролю, ответственности и трудозатратам и выбрать модель под проект
  • Описать выкатку rolling и blue-green с health check и мягким завершением старой версии
  • Спланировать миграцию БД в стиле expand/contract, чтобы откат кода не ломал данные
  • Составить чек-лист релиза с переменными окружения и планом отката

Проверено на

Nginx 1.30.5 (stable)Node.js 24 LTSDocker Engine 29.6

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Что значит «задеплоить»
  2. 02Три модели: VPS, PaaS и оркестрация контейнеров
  3. 03Выкатка без простоя: rolling, blue-green, canary
  4. 04Демонстрация: blue-green за Nginx на одном сервере
  5. 05Миграции БД: сначала расширить, потом сузить
  6. 06Переменные окружения и секреты
  7. 07Откат и чек-лист релиза

Что значит «задеплоить»

Деплой — это не «скопировать файлы на сервер», а цепочка из трёх стадий, которую методология The Twelve-Factor App называет build, release, run. Сборка превращает код в запускаемый артефакт: бандл, Docker-образ. Релиз соединяет сборку с конфигурацией конкретного окружения. Запуск поднимает процессы релиза и направляет на них трафик.

СборкаРелиз: сборка + конфигЗапуск рядом со старойHealth checkТрафик на новую
Готовим релизВыкатываем без простоя
Стадии деплоя: пока новая версия не прошла проверку, пользователей обслуживает старая.

Конфигурация, которая различается между окружениями — адреса баз данных, ключи внешних сервисов, домен, — по той же методологии хранится в переменных окружения, а не в коде. Один и тот же образ уходит в staging и production, меняются только переменные.

Три модели: VPS, PaaS и оркестрация контейнеров

VPS

Что даёт платформа
Виртуальный сервер с root-доступом
Что остаётся на команде
ОС и обновления, firewall, Nginx и HTTPS, бэкапы, мониторинг, сценарий выкатки
Когда уместна
Нужен полный контроль, нестандартный софт, долгоживущие процессы

PaaS: Vercel, Render, Fly.io

Что даёт платформа
Сборку из Git, HTTPS, выкатку, откат, логи
Что остаётся на команде
Код, переменные окружения, миграции, укладывание в ограничения платформы
Когда уместна
Небольшая команда, стандартный стек, важна скорость релизов

Контейнеры и Kubernetes

Что даёт платформа
Декларативные развёртывания, rolling update, проверки здоровья, откат ревизий
Что остаётся на команде
Кластер или управляемый сервис, манифесты, сеть, наблюдаемость
Когда уместна
Много сервисов и экземпляров, есть инженер, который отвечает за платформу

VPS — полный контроль и полная ответственность. Сервер ваш: можно поставить любой софт и держать WebSocket-соединения или фоновые процессы сколько угодно. Но и без простоя выкатывать, продлевать сертификаты, ставить патчи безопасности и восстанавливать сервер после сбоя тоже вам. Типичный стек — Docker и Nginx перед приложением, как в руководстве по reverse proxy.

PaaS (Platform as a Service) берёт эксплуатацию на себя, но задаёт свою модель. У Vercel каждый деплой — отдельная сборка со своим URL, а Instant Rollback возвращает production на прежний деплой. Render при каждом деплое поднимает новый экземпляр, переключает трафик и шлёт старому SIGTERM. Fly.io позволяет выбрать стратегию выкатки в fly.toml. Ограничения — время выполнения, файловая система, фоновые задачи — у каждой платформы свои, и их стоит прочитать до выбора, а не после.

Kubernetes решает задачу оркестрации: держит нужное число экземпляров, перезапускает упавшие, выкатывает новую версию постепенно и хранит историю ревизий для отката. Это мощный, но тяжёлый инструмент. По нашему опыту, одному-двум сервисам с одной базой чаще хватает VPS с Docker Compose или PaaS, а к Kubernetes приходят, когда сервисов много и появляется человек, отвечающий за платформу.

Берите PaaS, если

  • Команда маленькая и не хочет администрировать серверы
  • Стек поддерживается платформой из коробки
  • Нужны превью-окружения для каждой ветки
  • Ограничения платформы не мешают задаче

Берите VPS или контейнеры, если

  • Нужен нестандартный софт или доступ к ОС
  • Есть долгие процессы, постоянные соединения или локальный диск
  • Данные по требованиям должны жить в конкретной стране или у конкретного провайдера
  • Есть компетенция и время на эксплуатацию

Выкатка без простоя: rolling, blue-green, canary

Recreate

Как работает
Остановить все старые экземпляры, затем запустить новые
Плюсы
Просто, две версии никогда не работают одновременно
Минусы
Простой на время запуска

Rolling

Как работает
Заменять экземпляры по одному или группами
Плюсы
Не нужен двойной запас ресурсов
Минусы
Какое-то время работают обе версии

Blue-green

Как работает
Поднять полную копию новой версии и переключить весь трафик разом
Плюсы
Мгновенное переключение и такой же быстрый откат
Минусы
На время выкатки нужны ресурсы на две версии

Canary

Как работает
Сначала один экземпляр новой версии, затем остальные
Плюсы
Ошибка затрагивает малую часть запросов
Минусы
Нужны метрики, чтобы заметить проблему

Kubernetes Deployment по умолчанию использует стратегию RollingUpdate: параметры maxUnavailable и maxSurge по умолчанию равны 25%, то есть во время выкатки доступно не меньше 75% нужных подов и запущено не больше 125%. Альтернатива — Recreate. Fly.io по умолчанию выкатывает rolling, а также умеет immediate, canary и bluegreen; последние две не работают с машинами, к которым подключены тома, а для bluegreen нужен хотя бы один health check. Render для сервиса из нескольких экземпляров меняет их по одному и отменяет весь деплой, если новый экземпляр не стал здоровым.

  1. Health check

    Новая версия получает трафик только после проверки. В Kubernetes это readiness probe: под без готовности убирается из балансировки. Liveness probe решает другое — когда контейнер перезапустить, и документация предупреждает, что неаккуратная liveness-проверка может вызвать каскадные сбои.

  2. Переключение

    Балансировщик или прокси начинает отправлять новые запросы на новую версию. В Nginx это правка upstream и мягкая перезагрузка: старые рабочие процессы дообслуживают свои соединения.

  3. Мягкое завершение

    Старая версия получает SIGTERM, перестаёт принимать соединения, завершает текущие запросы и выходит. docker stop ждёт по умолчанию 10 секунд и затем шлёт SIGKILL; Render — 30 секунд, этот срок можно увеличить.

Демонстрация: blue-green за Nginx на одном сервере

Проверим разницу на практике. Стенд — Docker-сеть, Nginx 1.30.5 и Node.js-приложение из руководства по reverse proxy, которое дополнительно 3 секунды «прогревается» перед открытием порта, как реальный сервис при подключении к базе. Скрипт watch.sh изображает пользователей: шлёт запрос примерно каждые 50 мс и в конце считает ответы по коду и версии. Скрипты кладутся в одну папку с app/ и nginx/ и делаются исполняемыми: chmod +x *.sh. Все выводы ниже — реальные.

app/server.mjs
// simulated warm-up (DB connections, caches) before the port opens
const STARTUP_DELAY_MS = Number(process.env.STARTUP_DELAY_MS ?? 0);
setTimeout(() => {
  server.listen(PORT, () => console.log(`app ${VERSION} on :${PORT}`));
}, STARTUP_DELAY_MS);
Фрагмент: имитация прогрева перед listen — остальной код приложения как в руководстве по Nginx
nginx/conf.d/site.conf
include /etc/nginx/conf.d/upstream.inc;

server {
    listen 80;

    location / {
        proxy_pass http://app_backend;
        proxy_set_header Host            $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
Nginx берёт адрес активной версии из upstream.inc — этот файл и переписывает деплой
setup.sh
#!/usr/bin/env bash
# Local stand: one Docker network, the app v1 ("blue") and Nginx in front of it
set -euo pipefail
P="${PREFIX:-demo}"
docker network create "$P-net" >/dev/null
echo "upstream app_backend { server $P-app-blue:3000; }" > nginx/conf.d/upstream.inc
docker run -d --rm --name "$P-app-blue" --network "$P-net" \
  -e STARTUP_DELAY_MS=3000 -e APP_VERSION=v1 -v "$PWD/app:/app:ro" node:24-alpine node /app/server.mjs >/dev/null
docker run -d --rm --name "$P-proxy" --network "$P-net" -p 8081:80 \
  -v "$PWD/nginx/conf.d:/etc/nginx/conf.d:ro" nginx:stable-alpine >/dev/null
echo blue > .active
sleep 5 && curl -s http://localhost:8081/api/whoami && echo
Стенд: сеть, приложение v1 («синее») и Nginx на порту 8081
teardown.sh
#!/usr/bin/env bash
P="${PREFIX:-demo}"
docker stop "$P-proxy" "$P-app-blue" "$P-app-green" >/dev/null 2>&1
docker network rm "$P-net" >/dev/null 2>&1
rm -f .active nginx/upstream.prev
echo "stand removed"
Разбор стенда: остановить контейнеры (--rm удалит их) и удалить сеть
watch.sh
#!/bin/sh
# Simulated users: a request every ~50 ms for N seconds, then a summary
# of status codes and versions ("000" = no response within 2 s).
# Usage: ./watch.sh 20 http://localhost:8081/api/whoami
DURATION="${1:-20}"; URL="${2:-http://localhost:8081/api/whoami}"
END=$(( $(date +%s) + DURATION ))
while [ "$(date +%s)" -lt "$END" ]; do
  out=$(curl -s -m 2 -w ' %{http_code}' "$URL")
  code="${out##* }"
  version=$(printf '%s' "$out" | grep -o '"version":"[^"]*"' | cut -d'"' -f4)
  echo "$code ${version:--}"
  sleep 0.05
done | sort | uniq -c
Имитация пользователей: сводка по кодам ответа и версиям, 000 — ответа нет за 2 секунды

Сначала наивный вариант: остановить работающую версию и запустить новую на её месте.

naive.sh
#!/usr/bin/env bash
# Anti-pattern: stop the running version, then start the new one in its place
set -euo pipefail
P="${PREFIX:-demo}"
docker stop "$P-app-blue" >/dev/null
docker run -d --rm --name "$P-app-blue" --network "$P-net" \
  -e STARTUP_DELAY_MS=3000 -e APP_VERSION="$1" -v "$PWD/app:/app:ro" node:24-alpine node /app/server.mjs >/dev/null
echo "restarted in place with $1"
Антипаттерн: между остановкой и готовностью новой версии сервис недоступен
terminal
$ ./setup.sh
{"version":"v1","host":"localhost","forwardedFor":"192.168.16.1","peer":"::ffff:192.168.16.3"}
$ docker run --rm --network demo-net -v "$PWD/watch.sh:/watch.sh:ro" nginx:stable-alpine sh /watch.sh 15 http://demo-proxy/api/whoami > naive.txt &
$ sleep 3; ./naive.sh v2; wait
restarted in place with v2
$ cat naive.txt
     49 200 v1
    119 200 v2
     53 502 -
53 из 221 запроса получили 502: пока новая версия запускалась и прогревалась, отвечать было некому

Теперь blue-green: новая версия поднимается рядом, проходит проверку /healthz, Nginx переключается мягкой перезагрузкой, и только потом старая версия получает SIGTERM. Если проверка или nginx -t не прошли, трафик остаётся на старой версии.

deploy.sh
#!/usr/bin/env bash
# Blue-green release behind Nginx: start the new version next to the old one,
# wait for its health check, switch Nginx with a graceful reload, stop the old one.
# Usage: ./deploy.sh v2
set -euo pipefail

VERSION="$1"
P="${PREFIX:-demo}"                      # container name prefix
ACTIVE=$(cat .active 2>/dev/null || echo blue)
NEXT=$([ "$ACTIVE" = blue ] && echo green || echo blue)

echo "deploy $VERSION: $ACTIVE -> $NEXT"
docker run -d --rm --name "$P-app-$NEXT" --network "$P-net" \
  -e STARTUP_DELAY_MS=3000 -e APP_VERSION="$VERSION" -v "$PWD/app:/app:ro" \
  node:24-alpine node /app/server.mjs >/dev/null

# 1. Health check: the new version must answer before it gets traffic
for i in $(seq 1 20); do
  if docker exec "$P-proxy" curl -fsS "http://$P-app-$NEXT:3000/healthz" >/dev/null 2>&1; then
    echo "health check passed after $i attempt(s)"
    break
  fi
  if [ "$i" -eq 20 ]; then
    echo "health check failed: $NEXT stays out of traffic"
    docker stop "$P-app-$NEXT" >/dev/null
    exit 1
  fi
  sleep 0.5
done

# 2. Switch: rewrite the upstream, validate the config, reload gracefully
cp nginx/conf.d/upstream.inc nginx/upstream.prev
echo "upstream app_backend { server $P-app-$NEXT:3000; }" > nginx/conf.d/upstream.inc
if ! docker exec "$P-proxy" nginx -t >/dev/null 2>&1; then
  cp nginx/upstream.prev nginx/conf.d/upstream.inc
  docker stop "$P-app-$NEXT" >/dev/null
  echo "nginx -t failed: config restored"
  exit 1
fi
docker exec "$P-proxy" nginx -s reload
echo "$NEXT" > .active

# 3. Old version: SIGTERM, it finishes in-flight requests and exits
sleep 2
docker stop "$P-app-$ACTIVE" >/dev/null
echo "done: $VERSION is live on $NEXT, $ACTIVE stopped"
Blue-green деплой: health check, проверка конфига, мягкая перезагрузка Nginx, остановка старой версии
terminal
$ ./teardown.sh && ./setup.sh
stand removed
{"version":"v1","host":"localhost","forwardedFor":"192.168.16.1","peer":"::ffff:192.168.16.3"}
$ docker run --rm --network demo-net -v "$PWD/watch.sh:/watch.sh:ro" nginx:stable-alpine sh /watch.sh 20 http://demo-proxy/api/whoami > bg.txt &
$ sleep 3
$ docker run --rm --network demo-net nginx:stable-alpine curl -s -w ' %{http_code}\n' "http://demo-proxy/api/slow?ms=8000" > slow.txt &
$ sleep 0.5; ./deploy.sh v2; wait
deploy v2: blue -> green
health check passed after 4 attempt(s)
2026/10/12 01:36:08 [notice] 77#77: signal process started
done: v2 is live on green, blue stopped
$ cat bg.txt slow.txt
    126 200 v1
    134 200 v2
{"waitedMs":8000} 200
Ни одной ошибки за 260 запросов, а долгий запрос, начатый на v1 до переключения, завершился с кодом 200
terminal
$ docker run --rm --network demo-net -v "$PWD/watch.sh:/watch.sh:ro" nginx:stable-alpine sh /watch.sh 15 http://demo-proxy/api/whoami > rollback.txt &
$ sleep 3; ./deploy.sh v1; wait
deploy v1: green -> blue
health check passed after 5 attempt(s)
2026/10/12 01:36:29 [notice] 149#149: signal process started
done: v1 is live on blue, green stopped
$ cat rollback.txt
    108 200 v1
    126 200 v2
Откат — тот же деплой предыдущей версии: снова без ошибок

Миграции БД: сначала расширить, потом сузить

При rolling и blue-green старая и новая версии кода какое-то время работают с одной базой. Если миграция переименует колонку, старая версия упадёт, а откат кода не поможет: схема уже другая. Мартин Фаулер описывает выход как parallel change, или expand and contract: изменение делится на фазы, и на каждой схема совместима с обеими версиями кода.

  1. Expandрелиз 1

    Добавить новое, ничего не удаляя: новую колонку с допустимым NULL или значением по умолчанию, новую таблицу.

  2. Двойная записьрелиз 2

    Код пишет и в старое, и в новое поле, читает пока из старого. Существующие строки заполняются фоновой миграцией данных.

  3. Переключение чтениярелиз 3

    Код читает из нового поля. Откат на релиз 2 безопасен: старое поле всё ещё заполняется.

  4. Contractрелиз 4

    Когда старое поле никто не использует, удалить его отдельной миграцией.

Где запускать миграции, тоже зависит от платформы. У Render для этого есть pre-deploy command: она выполняется после сборки на отдельном экземпляре до выкатки, и при её ошибке деплой не происходит, а сервис продолжает работать на прежней версии; функция доступна на платных сервисах. У Fly.io — release_command во временной машине с новым образом: ненулевой код выхода отменяет деплой, таймаут по умолчанию 5 минут. На VPS миграцию запускают отдельным шагом CI перед переключением трафика.

Переменные окружения и секреты

По Twelve-Factor проверка простая: можно ли прямо сейчас открыть код репозитория, не раскрыв ни одного пароля? Если нет, конфигурация смешана с кодом. Ключи и адреса подставляются при запуске через переменные окружения или хранилище секретов платформы и не попадают ни в Git, ни в Docker-образ.

  • У Vercel изменение переменных окружения применяется только к новым деплоям, а Instant Rollback возвращает прежнюю сборку без учёта новых значений — после смены ключа нужен новый деплой
  • Значения переменных на Vercel шифруются при хранении, а секретные значения после сохранения доступны только на запись
  • На VPS переменные передают через файл окружения с ограниченными правами или менеджер секретов; файл .env добавляют в .gitignore
  • Для каждого окружения — свой набор ключей: production-ключи не используются в staging и на ноутбуках разработчиков

Откат и чек-лист релиза

Откат — это заранее отрепетированное действие, а не импровизация во время инцидента. В Kubernetes это kubectl rollout undo; по умолчанию Deployment хранит 10 старых ревизий, и при revisionHistoryLimit: 0 откатываться будет некуда. На Vercel — Instant Rollback, причём на тарифе Hobby только к непосредственно предыдущему деплою. На VPS — повторный деплой предыдущего тега образа, как в демонстрации.

  • Сборка неизменяема и имеет идентификатор: тег образа или ID деплоя, а не «последняя версия из main»
  • Миграции БД обратно совместимы и запускаются до переключения трафика
  • Новые переменные окружения и секреты добавлены во все нужные окружения
  • Health check проверяет реальную готовность, включая подключение к базе
  • Приложение обрабатывает SIGTERM: перестаёт принимать запросы и дообслуживает текущие
  • Известно, какой командой и за сколько шагов делается откат, и он хотя бы раз проверен
  • После переключения смотрим на ошибки и время ответа, а не только на зелёный статус деплоя
  • Старая версия живёт до конца проверки, чтобы откат был мгновенным
Выкатка без простоя — это не свойство хостинга, а порядок действий: поднять рядом, проверить, переключить, мягко остановить.
Как проверено2 · пройденосверено 11 октября 2026 г.
  • запущено, работает

    setup.sh, deploy.sh (blue-green), naive.sh, watch.sh, teardown.sh, nginx/conf.d/site.conf, app/server.mjs (прогрев)

    Docker 29.6.2, nginx:stable-alpine (1.30.5), node:24-alpine (v24.20.0), PREFIX=kb-deploy; watch.sh запускался в контейнере nginx:stable-alpine в той же сети против http://kb-deploy-proxy (в статье — префикс по умолчанию demo). Результаты последнего прогона: наивный перезапуск — 49×200 v1, 119×200 v2, 53×502; blue-green — 126×200 v1, 134×200 v2, 0 ошибок, запрос /api/slow?ms=8000, начатый до переключения, завершился 200; откат deploy.sh v1 — 108×200 v1, 126×200 v2, 0 ошибок. Health check проходил с 4–5 попытки (прогрев 3 с). Стенд удалён teardown.sh

  • запущено, работает

    Выводы shellsession (3 блока)

    реальный вывод scratchpad/kb/verify/deploj-prilozheniya/results.txt; вывод watch.sh и долгого запроса в прогоне перенаправлялся в файлы и печатался после деплоя — так и показано в статье

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

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