Деплой Next.js на Yandex Cloud Functions: рабочий путь
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 архитектура с контейнерами в большинстве случаев чище и надёжнее.