lenec ru

← все посты

Деплой Next.js на Yandex Cloud Functions: рабочий путь

15K

Yandex Cloud Functions — serverless-платформа в Yandex Cloud. По концепции близка к AWS Lambda: загружаешь архив с кодом, настраиваешь триггер, платишь за вызовы и время. Я разворачивал Next.js на Cloud Functions для пары проектов и хочу описать рабочую схему: что подходит, а что не очень.

Контекст: Next.js 14, App Router, серверные роуты и SSR-страницы. Yandex Cloud Functions с триггером API Gateway.

Сразу про ограничения

Не каждый Next.js-сайт можно бросить в Cloud Functions без оглядки. Особенности, о которых стоит помнить:

  • Время холодного старта. Next.js — большое приложение. Холодный старт может быть 1–3 секунды. На редких ручках это норма, на любых ручках, чувствительных к latency, — болезненно.
  • Размер архива. Cloud Functions поддерживает архив до 100 MB (несжатый — до 200 MB). Полный Next.js + node_modules легко превышает. Решается standalone-сборкой и аккуратной чисткой.
  • Память и timeout. Лимиты конфигурируются: до 4 GB RAM и до 10 минут выполнения. Для типичной SSR-страницы 512 MB и 30 секунд достаточно.
  • Файловая система. /tmp на 512 MB. Никакой персистентной файловой системы — нужно состояние, иди в БД или Object Storage.
  • WebSockets. Не поддерживаются. Если нужен real-time, Cloud Functions не подойдёт.

Если нужны WebSocket или ты не хочешь холодный старт — лучше Yandex Cloud Compute или Container Service. На просто SSR-сайте с REST API Functions работают вполне неплохо.

Standalone-сборка Next.js

Без неё node_modules затащит лишнее. В next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  images: {
    unoptimized: true, // Cloud Functions не подходит для on-the-fly image optim
  },
};

module.exports = nextConfig;

output: 'standalone' заставляет Next собрать в .next/standalone/ минимальный набор файлов: только нужные модули, без dev-зависимостей и кеша билдера.

После next build у нас:

  • .next/standalone/ — серверная часть.
  • .next/static/ — статика, которую нужно положить рядом или раздавать отдельно.
  • public/ — публичные файлы.

HTTP-handler для Cloud Functions

Cloud Functions для Node ожидает функцию-обработчик в формате:

exports.handler = async (event, context) => {
  return { statusCode: 200, body: 'Hello' };
};

Next.js по умолчанию сервит через свой HTTP-сервер (server.js в standalone). Чтобы заработало в Functions, нужен адаптер, который превращает event Yandex Cloud в Web Request и обратно.

Создаю handler.mjs рядом с собранным Next:

import next from './standalone/server.js';
import { Readable } from 'node:stream';

let appPromise;

async function getApp() {
  if (!appPromise) {
    process.env.HOSTNAME = '127.0.0.1';
    process.env.PORT = '3000';
    appPromise = import('./standalone/server.js');
  }
  return appPromise;
}

export async function handler(event, context) {
  // Cloud Functions API Gateway даёт event с httpMethod, path, headers, body, queryStringParameters
  const url = new URL(event.path + (event.url ?? ''), 'https://example.ru');
  if (event.queryStringParameters) {
    for (const [k, v] of Object.entries(event.queryStringParameters)) {
      url.searchParams.set(k, v);
    }
  }

  const req = new Request(url.toString(), {
    method: event.httpMethod,
    headers: event.headers,
    body: event.body ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8') : undefined,
  });

  const app = await getApp();
  const res = await app.handleRequest(req);

  const body = await res.text();
  return {
    statusCode: res.status,
    headers: Object.fromEntries(res.headers),
    body,
    isBase64Encoded: false,
  };
}

На практике standalone-сервер Next.js не экспортирует handleRequest напрямую — нужно поднять Node http-сервер на localhost и пробрасывать к нему запросы. Это можно сделать через готовые адаптеры (@codegenie/serverless-express, serverless-http) или написать свой обёртку. Я для упрощения часто использую serverless-http:

import http from 'node:http';
import serverless from 'serverless-http';

let handlerPromise;

async function makeHandler() {
  process.env.HOSTNAME = '127.0.0.1';
  process.env.PORT = '0';
  // Запускаем server.js standalone один раз и держим ссылку на http-сервер
  const { default: nextServerStart } = await import('./standalone/server.js');
  // ... здесь зависит от точной структуры standalone-выхода Next
  return serverless(nextServerStart);
}

export async function handler(event, context) {
  if (!handlerPromise) handlerPromise = makeHandler();
  const h = await handlerPromise;
  return h(event, context);
}

Точная сборка зависит от версии Next и конкретики проекта. На больших проектах легче поднять Container Service с Dockerfile, чем мучиться с адаптером. Functions — для небольших Next-приложений с few-route SSR.

Деплой

Архивирую и отправляю через CLI yc:

yc serverless function version create \
  --function-name nextjs-app \
  --runtime nodejs18 \
  --entrypoint handler.handler \
  --memory 1024m \
  --execution-timeout 30s \
  --source-path ./build.zip \
  --environment NODE_ENV=production

В сборочном скрипте:

#!/bin/bash
set -e

next build
rm -rf build
mkdir -p build
cp -r .next/standalone/* build/
cp -r .next/standalone/.next build/.next 2>/dev/null || true
cp -r .next/static build/.next/static
cp -r public build/public 2>/dev/null || true
cp handler.mjs build/handler.mjs
cp package.json build/package.json

cd build
zip -r ../build.zip . -x '*.map' '*.test.*' 'tests/*'
cd ..

ls -lh build.zip

На моих проектах итоговый zip получается около 30–50 MB. Если упираешься в 100 MB — режь dev-зависимости через --prod в pnpm install и проверяй, что не тащишь огромные пакеты типа @swc/core в рантайм.

API Gateway как триггер

Чтобы Cloud Function отвечала на HTTP-запросы по домену, нужен Yandex API Gateway. В простом виде он умеет роутить запросы на функцию по spec в формате OpenAPI:

openapi: 3.0.0
info:
  title: nextjs
  version: '1.0'
paths:
  /{proxy+}:
    x-yc-apigateway-any-method:
      x-yc-apigateway-integration:
        type: cloud_functions
        function_id: <function_id>
        service_account_id: <sa_id>
      parameters:
        - name: proxy
          in: path
          required: true
          schema: { type: string }

Этот spec говорит: «любой запрос на любой путь — отправляй в нашу функцию». Дальше в Yandex Cloud делается gateway, к нему привязывается домен с TLS, и сайт работает.

Статика и Object Storage

Раздавать .next/static/* и public/* через функцию — расточительно. Я заливаю их в Yandex Object Storage и раздаю напрямую через CDN (Yandex Cloud CDN или Cloudflare).

aws --endpoint-url=https://storage.yandexcloud.net s3 sync \
  ./public s3://my-site-static/public/ \
  --acl public-read --cache-control 'public, max-age=31536000, immutable'

aws --endpoint-url=https://storage.yandexcloud.net s3 sync \
  ./.next/static s3://my-site-static/_next/static/ \
  --acl public-read --cache-control 'public, max-age=31536000, immutable'

В next.config.js указываю assetPrefix, чтобы Next вёл ссылки на CDN:

module.exports = {
  output: 'standalone',
  assetPrefix: process.env.NODE_ENV === 'production' ? 'https://cdn.example.ru' : undefined,
};

Холодный старт: что помогает

  • Memory => CPU. Yandex Cloud Functions дают пропорциональную CPU-долю к памяти. На 256 MB старт болезненный, на 1024 MB заметно бодрее.
  • Минимум зависимостей. Каждый require/import добавляет к времени старта. Проверь, что не тащишь моники типа moment, lodash целиком и т. п.
  • Edge кеш через CDN. Большая часть запросов уходит в кеш и до функции не доходит — холодный старт ловят немногие.
  • Pre-warm. Cloud Functions нативно prewarm не умеют, но можно через триггер по расписанию пинать функцию каждые 5 минут — будет тёплая.

Когда не брать Functions

  • Сайт с тысячами SSR-запросов в минуту: дешевле виртуалка.
  • Real-time, WebSocket, SSE: Functions не для этого.
  • Тяжёлые ручки с долгими операциями (более минуты).
  • Полный Next.js с image optimization и десятками middleware: лучше контейнер.

Когда брать

  • Маленькое API + несколько SSR-страниц.
  • Нерегулярная нагрузка: иногда 0 запросов, иногда всплеск. Платишь только за работу.
  • Лендинг с парой динамических ручек (форма заявки, поиск).
  • Внутренние инструменты, которые можно подождать секунду.

Альтернативы внутри Yandex Cloud

Если функции не подошли:

  • Yandex Serverless Containers. Тот же serverless, но твой Docker-контейнер. Никаких ограничений Next-сборки. Холодный старт сравним.
  • Yandex Cloud Compute (виртуалка). Классический деплой через nginx + systemd. Дороже на низкой нагрузке, дешевле и предсказуемее на стабильной.
  • Managed Kubernetes. Если у вас уже есть k8s — деплой Next в нём — стандартная практика.

Что я выбираю по умолчанию

Под Next.js я почти всегда сейчас беру Serverless Containers, а не Cloud Functions. Это упрощает жизнь:

  • Никакого адаптера: Next.js слушает порт, контейнер просто запускается.
  • Размер образа гораздо больше, чем 100 MB лимит функции.
  • Тот же billing-принцип: платишь за работу, не за idle.
  • В Compute и в k8s легко мигрировать оттуда же.

Cloud Functions хороши для отдельных микро-API, обработчиков очередей, обёрток над БД. Для полноценного Next.js архитектура с контейнерами в большинстве случаев чище и надёжнее.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.