В начале у меня была одна цель — скорость. Хотелось написать код, нажать одну кнопку и сразу увидеть изменения на сайте. Деплой был под это заточен: никаких лишних церемоний.
Как было: быстро и хрупко
Всё держалось на одном скрипте. Он копировал рабочую папку на сервер по scp, там собирал образ, гасил старые контейнеры и поднимал новые. Иногда я просто заходил на сервер, делал git pull и docker compose up -d.
Это работало и было по-своему красиво — до того момента, когда что-то шло не так. Проблема в подходе: деплоилось не то, что проверено, а то, что лежало в рабочей папке в данный момент. Разница между этими вещами кажется мелочью, пока не наступит.
Провал, который всё изменил
Однажды сайт упал целиком. Не отдельная страница — весь сайт, все маршруты. Я открыл логи и увидел понятную картину: приложение стартовало и сразу падало, а nginx отказывался подниматься, пока приложение не станет здоровым.
Причина оказалась обидной. Локально я добавил монтирование новой папки — и она существовала на моей машине, но не была под контролем версий. На сервере её не было, приложение падало на старте, а вслед за ним не запускался nginx. Одна папка уронила весь продакшн.
Вывод был простой и неприятный: пока прод получает файлы из рабочей папки, а не из проверенного коммита, такие сюрпризы будут повторяться. А ещё стало понятно, что у процесса нет предохранителя: упавший старт никто не поймал и не откатил — сайт просто лежал.
Как стало: собрали → протестировали → выкатили
Я перестроил деплой под три явные фазы, и у каждой теперь есть гейт.
Собираем и тестируем локально
Сначала — проверка, что вообще можно выкатывать. Скрипт отказывается работать, если в дереве есть незакоммиченные изменения, и прогоняет тесты. Пока тесты красные — релиз не стартует. Это скучно, но именно эта скука убирает целый класс ошибок.
Доставляем именно проверенный коммит
Дальше код экспортируется из коммита, а не из папки: git archive берёт только то, что реально зафиксировано в репозитории. Никакие «случайные» локальные файлы на прод больше не попадают — та самая злополучная папка просто не сможет туда уехать.
Собираем образ там, где удобно, а не там, где дорого
Есть нюанс, который стоило понять сразу: моя машина и сервер — разной архитектуры. Собирать образ на сервере в такой ситуации — лишний риск и нагрузка, а у сервера ещё и мало диска. Поэтому образ собирается локально под архитектуру сервера, упаковывается и уезжает готовым. Сервер больше ничего не собирает — только загружает и запускает. Заодно исчезли зависимости от доступа к репозиторию и сборки на бою.
Проверяем результат, а не факт нажатия кнопки
Самый важный гейт. После выката процесс не считает дело сделанным по факту запуска контейнеров. Он ждёт, пока сайт реально отвечает, приложение в статусе «здорово» и — отдельно — что обслуживает именно новая версия. Для этого у приложения есть маленький эндпоинт, который сообщает свой текущий релиз. Если ожидание не заканчивается успехом, запускается автоматический откат на предыдущий релиз.
Теперь «выкатили» — это не «команды выполнились», а «новая версия подтверждённо обслуживает трафик».
Что добавилось по пути
Пара вещей, которые заметно снижают нервозность:
- Замок на деплой. Два релиза одновременно больше не запустятся — второй честно откажется.
- Откат — это перезапуск уже собранного. Предыдущий релиз остаётся на диске и может быть поднят мгновенно, без сборки.
- Ротация. Хранятся несколько последних релизов, старые автоматически удаляются — диск не зарастает.
- Изоляция. Пересобираются только приложение и веб-сервер; наблюдаемость и сервис уведомлений релиз не трогает.
И отдельная страховка от «медленной смерти»: на сервере появился swap. Не потому, что памяти мало в моменте, а потому что у любого пика должен быть запас. Лучше уйти в резервный своп, чем быть убитым системой в самый неподходящий момент.
Куда смотреть дальше
Процесс стал контролируемым, но он ещё не идеальный. Вот направления, которые я вижу.
Нулевой простой (blue-green). Сейчас пересоздание приложения и веб-сервера даёт короткий разрыв. Можно держать две версии приложения одновременно и переключать трафик между ними. Ресурсов — примерно второй контейнер — теперь хватает: именно ради этого и был добавлен swap.
По одной реплике (rolling update). Более мягкий вариант: несколько экземпляров приложения, которые обновляются по очереди, без полного перерыва.
Хранилище образов вместо передачи файлов. Сейчас готовый образ пересылается как архив. Реестр образов сделал бы шаг «доставка» чище, а откат — ещё быстрее.
Полный автомат на каждое изменение. Логичное продолжение — автоматический прогон тестов и сборки на каждый push, а выкат — по явному подтверждению. Тогда человек принимает только решение «можно в прод», а не выполняет механику.
Отдельный стенд для проверки. Тестовый контур, максимально похожий на прод, на котором релиз обкатывается до боевого выката. Тогда проверка перестаёт быть только автоматической и становится ещё и ручной, «глазами».
Что понял
Скорость деплоя и его безопасность — не враги. Враг — отсутствие гейтов. Сначала мне казалось, что проверки и откаты замедлят работу; на практике они убрали самый дорогой сценарий — разбираться ночью с лежащим сайтом. Быстрый деплой приятен ровно до первого падения. Контролируемый — приятен всегда.
Загрузка…