Когда ваш фронтенд требует сервера: построение CI/CD пайплайна для SSR-приложений

Вы только что закончили фичу в своём Next.js приложении. Сборка проходит локально. Вы пушите в продакшен. Но вместо рабочей страницы пользователи видят пустой экран с крутящимся спиннером. Серверный процесс запустился, но на самом деле не был готов обрабатывать запросы.

В этот момент вы осознаёте: деплой серверного фронтенда принципиально отличается от деплоя статического сайта. Пайплайн, который работал для вашего React SPA или статического Gatsby-сайта, больше не подходит.

Ключевое отличие: вы деплоите сервер, а не файлы

Со статическим фронтендом сборка создаёт HTML, CSS и JavaScript файлы. Вы загружаете их на CDN или в бакет — и готово. Деплой по сути является операцией копирования файлов.

При серверном рендеринге (SSR) результат сборки включает серверный код, который должен выполняться как процесс. Фреймворки вроде Next.js, Nuxt или Remix генерируют папку, содержащую:

  • JavaScript-код, выполняемый на сервере
  • JavaScript-бандлы для клиента
  • Статические ассеты: изображения, шрифты
  • Точку входа (часто server.js), запускающую приложение

Ваш пайплайн теперь должен рассматривать этот результат как приложение, работающее непрерывно, а не как набор файлов для раздачи. Это меняет всё: как вы собираете, тестируете и деплоите.

Шаг 1: Сборка с правильным таргетом

Шаг сборки на первый взгляд похож на статический фронтенд. Вы запускаете npm run build или эквивалентную команду фреймворка. Но результат отличается, и то, что вы с ним делаете, тоже.

Для SSR результат сборки нужно упаковать в нечто, способное работать на сервере. Если вы используете контейнеры, это означает создание Docker-образа, включающего:

  • Собранный серверный код
  • Зависимости времени выполнения (версия Node.js, системные библиотеки)
  • Файлы конфигурации, необходимые во время работы
  • Скрипт точки входа

Ваш Dockerfile может выглядеть так:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY .next ./.next
COPY public ./public
EXPOSE 3000
CMD ["node", ".next/standalone/server.js"]

Ключевая деталь: вы копируете не весь исходный код, а только то, что нужно для запуска приложения. Это уменьшает размер образа и снижает поверхность атаки.

Шаг 2: Health checks — не опция, а необходимость

Вот где многие SSR-пайплайны терпят неудачу. Контейнер запускается, процесс работает, и все считают, что приложение функционирует. Но «процесс запущен» не равно «приложение может обрабатывать запросы».

Ваше приложение может успешно запуститься, но не отображать страницы из-за:

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

Добавьте в приложение endpoint для health check. Обычно он находится по адресу /health или /api/health и возвращает статус 200, когда приложение действительно может обрабатывать запросы. Ваш пайплайн должен вызывать этот endpoint после деплоя, до того как направить трафик на новую версию.

Если health check не проходит — остановите пайплайн. Не давайте пользователям видеть страницы с ошибками или бесконечную загрузку. Команда разбирается, исправляет проблему и запускает пайплайн заново.

Шаг 3: Выберите стратегию деплоя

Есть два распространённых пути деплоя SSR-приложений: напрямую на сервер или в контейнеры. Каждый имеет разные последствия для вашего пайплайна.

Следующая диаграмма иллюстрирует полный SSR-пайплайн: от сборки до деплоя с критической точкой принятия решения — health check.

flowchart TD A[Сборка SSR приложения] --> B[Упаковка артефакта] B --> C[Деплой на сервер/в кластер] C --> D[Запуск health check] D --> E{Health check пройден?} E -->|Да| F[Направление трафика на новую версию] E -->|Нет| G[Остановка пайплайна и оповещение] F --> H[Отслеживание работающей версии] G --> I[Расследование и исправление] I --> A H --> J[Мониторинг и логирование]

Прямой деплой на сервер

Вы копируете результат сборки на сервер, останавливаете старый процесс и запускаете новый. Ключевая проблема — как выполнить переход.

Если убить старый процесс немедленно, запросы, которые обрабатываются в данный момент, упадут. Пользователи увидят ошибки. Решение — graceful shutdown: старый сервер перестаёт принимать новые запросы, но завершает обработку уже начатых. Когда они завершаются, процесс чисто завершается. Затем запускается новый сервер и начинает принимать трафик.

Ваш скрипт пайплайна должен координировать эту передачу. Это выполнимо, но требует тщательного скриптования и мониторинга.

Деплой в контейнеры

Контейнеры дают больше контроля. Пайплайн собирает новый Docker-образ, пушит его в registry и деплоит на платформу оркестрации контейнеров.

Вот минимальный Dockerfile, упаковывающий собранное SSR-приложение для контейнерного деплоя:

FROM node:20-alpine

WORKDIR /app

# Копируем продакшен-зависимости
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Копируем собранный сервер и клиентские ассеты
COPY .next ./.next
COPY public ./public

# Открываем порт, который слушает приложение
EXPOSE 3000

# Запускаем сервер
CMD ["node", ".next/standalone/server.js"]

В Kubernetes это превращается в rolling update:

  1. Новый pod запускается с новым образом
  2. Pod выполняет health check
  3. Как только он здоров, трафик постепенно переключается на новый pod
  4. Старые pod'ы завершаются после обработки текущих запросов

Kubernetes автоматически обрабатывает graceful shutdown и переключение трафика. Ваш пайплайн должен только обновить манифест деплоя с новым тегом образа и применить его.

Шаг 4: Отслеживайте работающую версию

После деплоя пайплайн должен записать, какая версия запущена. Сохраните хеш коммита, тег образа или метку времени деплоя в доступном месте. Эта информация бесценна, когда нужно:

  • Откатиться на предыдущую версию
  • Выяснить, какой деплой привёл к багу
  • Связать проблемы производительности с конкретными релизами

Простой подход: тегируйте Docker-образы хешем коммита и храните соответствие в базе данных или простом текстовом файле. Ваши инструменты мониторинга смогут ссылаться на эти данные при оповещениях.

Практический чек-лист для вашего SSR-пайплайна

Прежде чем назвать пайплайн готовым к продакшену, проверьте эти пункты:

  • Результат сборки упакован в деплоимый артефакт (Docker-образ или серверный пакет)
  • Существует endpoint для health check, возвращающий осмысленный статус
  • Пайплайн ждёт прохождения health check перед направлением трафика
  • Пайплайн останавливается и оповещает, если health check не пройден
  • Настроен graceful shutdown (старый процесс завершает начатые запросы)
  • Стратегия rolling update протестирована (нет даунтайма при деплое)
  • Информация о версии сохранена и доступна после деплоя
  • Процесс отката задокументирован и протестирован

Вывод

SSR-фронтенд — это не статический сайт. Это серверное приложение, которое, помимо прочего, рендерит HTML. Относитесь к своему пайплайну соответственно: собирайте для рантайма, проверяйте health checks, деплойте с нулевым даунтаймом и всегда знайте, какая версия обслуживает пользователей. Когда эти основы настроены правильно, пользователи никогда не увидят пустой экран. Они просто видят быструю, работающую страницу.