Флаг: English English Флаг: Español Español

Современный деплой Next.js: GitHub Actions, Docker и Zero-Downtime

Опубликовано 02.03.2026

Если ты до сих пор делаешь next build прямо на продакшен-сервере — твой сервер действительно страдает. CPU в полку, OOM-kill, 502-е ошибки и долгие простои — это классика, которой пора положить конец.

В 2026 году стандарт индустрии — это раздельная сборка:

  1. Собираем минимальный standalone-образ в облаке GitHub Actions.
  2. Пушим его в GHCR (GitHub Container Registry).
  3. На сервере делаем только pull + атомарный перезапуск.

Глава 1. Идеальный Dockerfile (Multi-stage + Standalone)

Весь секрет маленького и быстрого образа — в режиме standalone. Next.js сам вычисляет, какие файлы и части node_modules реально нужны для работы сервера, и копирует только их.

# syntax=docker/dockerfile:1
# СТАДИЯ 1 — Зависимости
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json yarn.lock* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else npm ci; \
  fi

# СТАДИЯ 2 — Сборка
FROM node:24-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# СТАДИЯ 3 — Production-образ (Runner)
FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Безопасность прежде всего: работаем не под root
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs

# Копируем только артефакты standalone-сборки
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# Проверка здоровья контейнера
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1

CMD ["node", "server.js"]

Почему это круто?

  • Вес: Образ весит ~200 МБ против 1.5 ГБ обычного.
  • Безопасность: Использование non-root пользователя (nextjs) защищает хост-систему при взломе контейнера.
  • Healthcheck: Docker сам поймет, если приложение «зависло» на старте, и не пустит на него трафик.

Глава 2. Разбор GitHub Actions Workflow

Твой пайплайн разделен на два этапа (jobs): сборка в облаке и деплой на твоем “железе”.

1. Подготовка и билд

- name: Docker meta & tags
  id: prepare
  run: |
    IMAGE_REPO="ghcr.io/${GITHUB_REPOSITORY,,}"
    SHORT_SHA="${GITHUB_SHA::8}"
    echo "image_repo=$IMAGE_REPO" >> $GITHUB_OUTPUT
    echo "image_tag=sha-$SHORT_SHA" >> $GITHUB_OUTPUT

Важный нюанс: Синтаксис ${GITHUB_REPOSITORY,,} приводит название репозитория к нижнему регистру. Docker-реестры не переваривают заглавные буквы, а в GitHub они часто встречаются.


2. Кэширование сборки

cache-from: type=gha
cache-to: type=gha,mode=max

Мы используем нативное кэширование GitHub Actions. Если ты не менял package.json, стадия установки зависимостей пропустится, и билд займет 1–2 минуты вместо 10.


3. Умный деплой (Healthcheck Loop)

Самая важная часть — мы не просто говорим серверу “обновись”, мы проверяем, выжило ли приложение.

for i in {1..60}; do
  STATUS=$(docker inspect --format='{{json .State.Health.Status}}' nextjs 2>/dev/null || echo '"not-found"')
  if [[ $STATUS == '"healthy"' || $STATUS == '"no-healthcheck"' ]]; then
    HEALTHY=1
    break
  fi
  sleep 2
done

Если Next.js упадет из-за ошибки в переменных окружения, скрипт увидит это, не обновит прокси-сервер (Caddy/Nginx) и завершит экшен с ошибкой. Твой старый сайт останется работать, а ты получишь уведомление о проблеме.


Глава 3. Runtime vs Build-time переменные

Это “грабли”, на которые наступают почти все.

  1. NEXT_PUBLIC_ (Build-time): Эти переменные “впекаются” в JS-бандл во время команды next build. Если ты поменяешь их на сервере в .env, ничего не изменится. Их нужно передавать в GitHub Actions как build-args.

  2. Секреты (Runtime): DATABASE_URL, JWT_SECRET. Их нельзя класть в Docker-образ. Они подтягиваются в момент запуска контейнера через docker-compose.

Совет: По возможности делай API URL также runtime-переменной через проксирование или специальные конфиг-скрипты, чтобы один и тот же образ можно было катить и на стейджинг, и в прод без пересборки.

Отзывы по теме

Было несколько проблем касаясь как технической части так и понимания в целом. Михаил быстро ответил на запрос, помог разобраться и решил проблеммы технические и помог разобраться в понимании, за что отдельное спасибо. Результатом доволен.

abazawolf · Настройка vps, настройка сервера

18.02.2026 · ⭐ 5/5

Было несколько проблем касаясь как технической части так и понимания в целом. Михаил быстро ответил на запрос, помог разобраться и решил проблеммы технические и помог разобраться в понимании, за что отдельное спасибо. Результатом доволен.

Нужна помощь?

Свяжись со мной и я помогу решить проблему

Похожие посты