Выкат на VM — реестр, релизы, откат
Перед началом: нагрузка и метрики (Модуль 11). Инструменты: Docker, реестр образов, nginx, ssh, Linux-VM. Трудоёмкость: ~8 ч.
🔥 Инцидент: «Задеплоил — а ничего не изменилось»
Релизный день. На VM выполнили docker compose build && up -d — вывод
пустой, код 0, контейнеры подняты. Только через час выяснилось: compose
на сервере собрал старые образы — legacy builder молча не пересобрал
слои, и exit code был честный ноль. Параллельно выяснилась вторая
подлость: сам процесс сборки на VM съел CPU, и сервис, который «должен
был обновиться», всё это время тормозил под нагрузкой сборки. Вывод,
которым живёт рабочий стенд, на который мы равнялись: на сервер ничего
не собирается — сервер только тянет готовые образы. В этом модуле
строим такой конвейер: сборка → реестр → pull → health-гейт → откат.
📖 Теория
12.1 Три принципа релиза
- Сборка — на машине разработчика (или в CI), на VM только
pull. Никакой конкуренции сборки за CPU с сервисом и никаких «молчаливых» пересборок. - Версия — в теге образа, тег — в
.env.BACKEND_TAG(api+migrate — одна версия) иUI_TAG(статика отдельной жизнью: релиз UI не рвёт WS-соединения). Откат = поменять тег иup -d. - Релиз проходит гейт. После
up -dпроверяем, что поднялось работающее:/healthAPI и корень UI (ожидаем HTML-страницу, а не200-заглушку прокси). Красный гейт → авто-откат на предыдущий тег.
12.2 Состав стека на VM
Наш стадия-1 мессенджер в одном Go-процессе (REST + WS), поэтому compose небольшой:
| Сервис | Роль |
|---|---|
postgres | данные (volume на bind-mount) |
redis | presence, typing, rate limit (уже умеет деградировать) |
migrate | one-shot: up при выкате, до старта api |
api | REST + WebSocket, одна реплика |
ui | nginx: статика SPA + реверс-прокси /api и /ws |
backup | ежедневный pg_dump + ротация |
Наружу — только 80/443. Postgres и Redis — во внутренней сети compose, без портов наружу (в dev-схеме они открыты на 5433/6380 — это только для локальной разработки).
12.3 nginx: два неочевидных требования к WS
Конфиг проксирования WebSocket — место, где ломаются 90% первых деплоев:
nginxlocation /api/v1/ws { proxy_pass http://api:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # без этого — не апгрейд proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 3600s; # иначе обрыв каждые 60с proxy_buffering off; # события — не буферизовать }
proxy_read_timeout по умолчанию — 60 секунд: WS «умирает» раз в минуту
и клиенты переподключаются. Симптом выглядит как «сервер нестабилен», а
лечится одной строкой.
12.4 Выпуск образов
deploy/build-push.sh (на машине разработчика):
bash#!/usr/bin/env bash set -euo pipefail TAG="${1:-$(git describe --tags --always)}" REGISTRY="${REGISTRY:-ghcr.io/<you>/messenger}" docker build -t "$REGISTRY-backend:$TAG" backend/ docker build -t "$REGISTRY-ui:$TAG" frontend/ docker push "$REGISTRY-backend:$TAG" && docker push "$REGISTRY-ui:$TAG" echo "Выкат: ./deploy/release.sh --component all \ --backend-tag $TAG --ui-tag $TAG"
Печатать готовую команду выката — мелочь, которая убирает класс ошибок «выкатил не тот тег». Реестр — GHCR (просто) или Yandex Container Registry (как на взрослых стендах, но с настройкой сервисного аккаунта): выбор фиксируйте в README, механизм одинаков.
12.5 release.sh: только механика
Скрипт на VM умеет ровно одно: pull + up для выбранных компонентов. Решение «гейт зелёный / откатывать» принимает вызывающий (человек, CI или маленький оркестратор):
bash#!/usr/bin/env bash # release.sh --component all|api|ui --backend-tag T --ui-tag T [--dry-run] set -euo pipefail # ... парсинг аргументов ... [ "$COMPONENT" != "ui" ] && sed -i "s/^BACKEND_TAG=.*/BACKEND_TAG=$BACKEND_TAG/" .env [ "$COMPONENT" != "api" ] && sed -i "s/^UI_TAG=.*/UI_TAG=$UI_TAG/" .env docker compose --env-file .env pull [ "$COMPONENT" != "ui" ] && docker compose --env-file .env run --rm migrate docker compose --env-file .env up -d --remove-orphans echo '{"component":"'"$COMPONENT"'","backend_tag":"'"$BACKEND_TAG"'"}"
Разделение «механика/решение» важно: механика не знает про здоровье — она
просто идемпотентно приводит систему к тегам из .env.
12.6 Гейт и откат
bash# после release.sh: curl -fsS http://127.0.0.1/health # API жив (через nginx) curl -fsS http://127.0.0.1/ | grep -q '<div id="root"' # UI — это SPA, не заглушка # красный гейт → откат: ./release.sh --component all --backend-tag "$PREV_BACKEND" --ui-tag "$PREV_UI"
Скрипт-оркестратор хранит предыдущие теги (файл deploy/.env.prev),
пишет результат гейта в лог и завершает релиз статусом
released | rolled_back. По этим строкам потом видно, что происходило.
12.7 Миграции expand→contract — теперь это ваша схема
Помните, почему rooms → conversations делали в три шага (Модуль 2)?
Вот здесь это окупается: откат на предыдущий образ после прогона
миграций не падает о схему — старый код просто не замечает новых колонок
и таблиц. Порядок релиза: migrate (expand) → api → проверка → в
следующем релизе contract (удаление старого).
12.8 stg, TLS, бэкапы, CI
- stg: локальный
docker compose— учебный stg-контур; на проде второй копией стека на той же VM (другие порты и БД) проверяют релиз до прода. - TLS: certbot (
--nginx) или Caddy (авто-TLS «из коробки») — оба пути валидны; главное — HTTPS иwss://, без них push не работает. - Бэкапы: контейнер
backupсpg_dumpраз в сутки в bind-mount; проверка бэкапа обязательна: раз в неделю restore в тестовую БД. - CI: GitHub Actions — по git-тегу собирает и пушит оба образа, затем вызывает выкат по ssh; health-гейт решает судьбу релиза.
💻 Практика
-
Соберите
deploy/:backend.Dockerfile(multi-stage: build → distroless или alpine),frontend.Dockerfile(build → nginx со статикой),nginx.conf(§12.3),docker-compose.prod.yml,.env.example(BACKEND_TAG,UI_TAG, секреты),build-push.sh,release.sh. -
Поднимите VM (любая Linux с Docker; Yandex Cloud/любой провайдер). Поток безопасности: только 22/80/443, пользователь деплоя, bind-mount
/var/lib/messenger/{pg,backups}. DNS-имя → статический IP. -
Первый деплой: выпустите
v0.1.0черезbuild-push.sh, на VM —cp .env.example .env, заполните,release.sh --component all. Настройте TLS, откройтеhttps://ваш-доменс телефона — это ваш прод. -
Релиз с проверкой отката:
- выпустите
v0.1.1со сломанным/health(нарочно: пусть отвечает 500 при старте); - прогоните
release.sh→ гейт красный → откат наv0.1.0→ гейт зелёный. Зафиксируйтеreleased | rolled_backв логе. - почините и выпустите
v0.1.2штатно.
- выпустите
-
Бэкап: дождитесь
pg_dump(или запустите вручную), восстановите дамп в тестовую БД, сравните число сообщений — бэкап без проверки не бэкап. -
CI (по желанию, но рекомендовано): workflow «тег → build-push → deploy по ssh», секреты — в настройках репозитория.
Чек-лист модуля: https://ваш-домен работает снаружи; WS живёт через
nginx (wss://) без обрывов; релиз v0.1.x прошёл гейт; смоделированный
сбой откатился автоматически; бэкап восстановлен в тестовую БД;
release.sh идемпотентен (повторный запуск с теми же тегами ничего не
ломает).
🧩 Quiz
proxy_read_timeout 3600s для WS в nginx?✅ Итоги модуля
- Релизный конвейер стоит на трёх принципах: сборка на dev-машине, версии в тегах
.env, гейт с откатом. - Стек на VM — шесть контейнеров; наружу только 80/443; nginx настроен под WS.
release.sh— чистая механика, решения принимает вызывающий.- Expand→contract из Модуля 2 оказался не теорией, а страховкой откатов.
- Бэкап проверен восстановлением; CI замыкает контур «тег → прод».
🔗 Мостик к Модулю 13
Продукт живёт по адресу, релизы откатываются, бэкапы проверены. Осталось главное — пройти защиту: показать систему с двух устройств, обосновать решения перед «комиссией» и закрыть чек-лист приёмки. Финальный модуль — ваш регламент сдачи.