Когда ваш фронтенд требует сервера: построение 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.
Прямой деплой на сервер
Вы копируете результат сборки на сервер, останавливаете старый процесс и запускаете новый. Ключевая проблема — как выполнить переход.
Если убить старый процесс немедленно, запросы, которые обрабатываются в данный момент, упадут. Пользователи увидят ошибки. Решение — 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:
- Новый pod запускается с новым образом
- Pod выполняет health check
- Как только он здоров, трафик постепенно переключается на новый pod
- Старые pod'ы завершаются после обработки текущих запросов
Kubernetes автоматически обрабатывает graceful shutdown и переключение трафика. Ваш пайплайн должен только обновить манифест деплоя с новым тегом образа и применить его.
Шаг 4: Отслеживайте работающую версию
После деплоя пайплайн должен записать, какая версия запущена. Сохраните хеш коммита, тег образа или метку времени деплоя в доступном месте. Эта информация бесценна, когда нужно:
- Откатиться на предыдущую версию
- Выяснить, какой деплой привёл к багу
- Связать проблемы производительности с конкретными релизами
Простой подход: тегируйте Docker-образы хешем коммита и храните соответствие в базе данных или простом текстовом файле. Ваши инструменты мониторинга смогут ссылаться на эти данные при оповещениях.
Практический чек-лист для вашего SSR-пайплайна
Прежде чем назвать пайплайн готовым к продакшену, проверьте эти пункты:
- Результат сборки упакован в деплоимый артефакт (Docker-образ или серверный пакет)
- Существует endpoint для health check, возвращающий осмысленный статус
- Пайплайн ждёт прохождения health check перед направлением трафика
- Пайплайн останавливается и оповещает, если health check не пройден
- Настроен graceful shutdown (старый процесс завершает начатые запросы)
- Стратегия rolling update протестирована (нет даунтайма при деплое)
- Информация о версии сохранена и доступна после деплоя
- Процесс отката задокументирован и протестирован
Вывод
SSR-фронтенд — это не статический сайт. Это серверное приложение, которое, помимо прочего, рендерит HTML. Относитесь к своему пайплайну соответственно: собирайте для рантайма, проверяйте health checks, деплойте с нулевым даунтаймом и всегда знайте, какая версия обслуживает пользователей. Когда эти основы настроены правильно, пользователи никогда не увидят пустой экран. Они просто видят быструю, работающую страницу.