Next.js хостится самостоятельно двумя командами: next build создаёт каталог .next, а next start отдаёт его через HTTP-сервер Node на выбранном вами порту. Всё остальное, что идёт не так, - следствие четырёх фактов. Сборке нужно гораздо больше памяти и CPU, чем работающему приложению. Сборке нужны devDependencies, которые не нужны работающему приложению. Часть переменных окружения вшивается в результат сборки, а часть читается во время выполнения, и ничто не подскажет, какие из них какие. И кэш, благодаря которому быстро работает инкрементальная статическая регенерация, - это каталог на диске, поэтому у него есть свои требования к перезапускам и к работе в нескольких экземплярах.
Ниже разобрано, что каждый из этих пунктов значит на одном сервере за обратным прокси, а это обычный способ запускать Next.js, когда вы не на платформе, для которой он написан.
Сборка и запуск: две команды с очень разным аппетитом#
$ npm ci # devDependencies included, on purpose$ npm run build # next build$ npm run start -- -p 3000 -H 0.0.0.0Первый сюрприз в том, что npm ci --omit=dev && npm run build не работает. TypeScript, Tailwind, PostCSS, ESLint и ваши пакеты с типами - это devDependencies, а next build в них нуждается. Либо ставьте всё и собирайте на сервере, либо собирайте где-то ещё и доставляйте результат. Почему npm ci остаётся правильным установщиком в обоих случаях, объясняет статья npm ci или npm install.
Второй - расход ресурсов. Компиляция и проверка типов приложения среднего размера может держать заметно больше гигабайта кучи и задействует все найденные ядра, тогда как то же приложение, обслуживающее трафик, после этого умещается в пару сотен мегабайт. Сборка в контейнере на 1 ГБ - классический сбой: она заканчивается JavaScript heap out of memory, либо контейнер останавливается на своём лимите, лог просто обрывается, а код выхода - 137. Ни одно из сообщений не говорит «купите больше памяти на девяносто секунд», но именно это оно и означает.
Три выхода в порядке возрастания стоимости:
- Собирайте в CI и деплойте результат. Сервер только ставит production-зависимости и запускает. Самый быстрый и наименее неожиданный вариант, и его стоит выбрать, если CI у вас уже есть.
- Временно собирайте на более крупном тарифе. Переход на уровень выше меняет лимит на уже существующем сервере, так что тариф, щедрый для сборки и достаточный для приложения, - законный ответ, когда сборки редки.
- Ограничить кучу и надеяться.
NODE_OPTIONS=--max-old-space-size=1536заставляет V8 агрессивнее собирать мусор, а не расти к потолку, которого у него нет. Часть сбоев она превращает в медленные сборки. Память она не создаёт. Два задействованных потолка объясняет статья почему ваше приложение Node падает на 2 ГБ при тарифе 4 ГБ.
Время сборки на дробной доле CPU - вторая половина компромисса. Сборка, занимающая 40 секунд на ноутбуке, может занять несколько минут на половине ядра, а это несколько минут простоя, если вы собираете на том же сервере, который обслуживает сайт.
Standalone-вывод и две папки, которые он не копирует#
Если задать output: "standalone" в next.config.js, сборка выдаёт самодостаточный каталог: небольшой server.js и только те файлы node_modules, которые трассировка сочла необходимыми. Для деплоя это обычно на сотни мегабайт меньше, чем перенос всего дерева.
const nextConfig = { output: "standalone", compress: false, // the proxy in front already does this poweredByHeader: false,};export default nextConfig;Все спотыкаются на том, что standalone-сборка намеренно не копирует два каталога, поскольку предполагается, что их раздаёт CDN: public и .next/static. Если перенести папку standalone куда-то и запустить как есть, сайт загрузится без CSS, без JavaScript и без изображений, а консоль заполнится ошибками 404 для /_next/static/.... Скопируйте их внутрь:
$ cp -r public .next/standalone/public$ cp -r .next/static .next/standalone/.next/static$ node .next/standalone/server.jsСгенерированный сервер читает PORT и HOSTNAME из окружения. Задайте оба явно, а HOSTNAME=0.0.0.0 - в особенности, потому что сервер, привязанный к loopback внутри контейнера, недоступен для прокси, и сбой выглядит как поломка приложения, а не как сетевая настройка. Заметьте также, что next start - это не способ запуска standalone-сборки; сама сборка предписывает запускать server.js напрямую.
Standalone стоит использовать, когда мало места на диске или времени на установку. Когда их хватает, это ещё одна движущаяся часть, а в monorepo ему нужно указать корень трассировки файлов вывода на корень workspace, иначе трассированный бандл не увидит пакеты, лежащие вне каталога приложения.
Переменные окружения: вшиваются при сборке, читаются при запуске#
Это самая запутанная вещь в self-hosting Next.js, и в основе у неё простое правило.
| Переменная | Когда читается | Для изменения нужно |
|---|---|---|
NEXT_PUBLIC_* | Встраивается в клиентский бандл при сборке | Пересборка |
| Серверные переменные в динамическом коде | На каждый запрос | Перезапуск |
| Серверные переменные на статически пререндеренных страницах | При сборке, в HTML | Пересборка |
NEXT_PUBLIC_API_URL - это не переменная в развёрнутом приложении. Это строка, которая была подставлена в ваш JavaScript во время сборки. Если задать другое значение на вкладке Startup и перезапустить, не изменится ничего, потому что менять уже нечего. Если значение должно отличаться между staging и production и используется в браузере, либо собирайте дважды, либо получайте его во время выполнения из маршрута API.
Обратная ловушка - серверная переменная, используемая на странице, которая была статически пререндерена. Значение, бывшее на момент сборки, лежит в HTML. Сделайте маршрут динамическим или читайте значение в пути с областью действия запроса, если оно должно быть актуальным.
Секреты без префикса NEXT_PUBLIC_ браузеру не открываются, в этом и весь смысл префикса, и им место в переменных окружения на вкладке Startup, а не в репозитории. Общий довод изложен в статье где хранить секреты на сервере приложений, и здесь он действует с удвоенной силой, потому что утёкшее значение NEXT_PUBLIC_ уже лежит в браузере каждого посетителя.
Кэш: ISR, ревалидация и что теряется при перезапуске#
Инкрементальная статическая регенерация рендерит страницу один раз, отдаёт сохранённую копию и заново рендерит её в фоне, когда та устаревает. При self-hosting это хранилище - каталог на диске в .next/cache. Отсюда три следствия.
Новая сборка - это новый кэш. У каждой сборки свой идентификатор, поэтому страницы, сгенерированные предыдущим деплоем, не используются повторно. Первый посетитель каждого маршрута после деплоя платит за рендер. На сайте с множеством маршрутов ISR это заметный холодный период, а значит, важные страницы стоит прогреть несколькими запросами после деплоя, а не оставлять их на долю первого реального пользователя.
Перезапуски безопасны, эфемерные диски - нет. Кэш переживает перезапуск, потому что каталог сохраняется. Пересборку он не переживает, а если файловая система выбрасывается между запусками, его нет вообще. На обычном сервере с постоянным хранилищем это не проблема, и это одно из тихих преимуществ запуска Next.js на обычной машине.
Больше одного экземпляра - больше одного кэша. Два процесса держат каждый свой, поэтому один и тот же URL может быть устаревшим в одном и свежим в другом. Для этого в next.config.js есть параметр cacheHandler: направьте его на общее хранилище, и все экземпляры согласуются. На одном сервере он вам не нужен, а имя этого параметра менялось между релизами, поэтому, если будете его применять, проверьте документацию для вашей мажорной версии.
Сама ревалидация бывает двух видов: по времени, задаваемой через export const revalidate = 3600 на маршруте или для отдельного fetch, и по требованию, через revalidatePath или revalidateTag, вызываемые из обработчика маршрута или серверного действия после изменения данных. Для контента, который меняется, когда человек нажимает «сохранить», почти всегда лучше вариант по требованию.
next/image и CPU, который никто не закладывал в бюджет#
next/image оптимизирует изображения по требованию в работающем сервере: меняет размер, перекодирует в современный формат и кэширует результат в .next/cache/images. Это реальный CPU и реальный диск на том же контейнере, что рендерит ваши страницы.
- Установите
sharp. Без него оптимизатор заметно медленнее, и Next предупреждает об этом при запуске. - Для внешних источников нужно настроить
images.remotePatterns, иначе запросы падают с понятным сообщением о ненастроенном имени хоста. - Кэш индексируется по источнику, ширине и качеству, так что компонент, запрашивающий восемь размеров одного hero-изображения, порождает восемь кодирований. Сколько вариантов может существовать, регулируют
deviceSizesиimageSizes. minimumCacheTTLрешает, как долго оптимизированный файл хранится, прежде чем будет создан заново.unoptimized: trueотключает всё это, и это правильный ответ, когда ваши изображения уже нужного размера и раздаются откуда-то ещё.
На тарифе с половиной ядра страница с дюжиной неоптимизированных исходных изображений сделает первый запрос медленным и заполнит каталог кэша перекодированными файлами. Это самая частая причина, по которой self-hosted сайт на Next локально ощущается нормально, а на небольшом сервере - вяло. Здесь неожиданным образом уместна статья что на самом деле меняет NVMe: кодирование упирается в CPU, так что более быстрый диск его не спасёт.
Что должен делать прокси перед приложением#
Собственный сервер Next обрабатывает HTTP, а прокси перед ним отвечает за TLS и имя хоста. На RE:NODE это слот proxy, входящий в каждый тариф приложений: направьте запись A на показанный адрес, и сертификат будет выпущен и продлён автоматически в окне в 21 день, а адрес клиента передаётся в X-Forwarded-For. Две половины разобраны в статьях что на самом деле делает обратный прокси и как направить домен на сервер.
На этой границе важно сделать правильно четыре вещи:
- Сжатие один раз. Next по умолчанию сжимает ответы. Если сжимает и прокси, задайте
compress: falseвnext.config.jsи пусть этим занимается прокси, иначе вы платите дважды за одни и те же байты. - Протокол в пересылаемых заголовках. Код, строящий абсолютные URL, должен знать, что исходный запрос был по HTTPS. Читайте пересылаемые заголовки, а не предполагайте, иначе ваши canonical-ссылки и редиректы будут указывать на
http://. - Стриминг. Ответы App Router передаются потоком, а границы Suspense приходят по частям. Прокси, полностью буферизующий ответы, превращает это в один поздний ответ: страница всё равно работает, но выигрыш в воспринимаемой скорости исчезает.
- Статические ресурсы. Файлы в
/_next/staticсодержат хеш содержимого и неизменяемы, поэтому их можно жёстко и надолго кэшировать. Какие заголовки для этого нужны, описано в статье HTTP-заголовки кэширования простыми словами, а в статье TTFB, Core Web Vitals и хостинг честно сказано, какие части оценки Lighthouse хостинг действительно способен сдвинуть.
Деплой и перезапуск#
Команда запуска на хосте, который собирает на самом сервере, выглядит так:
npm ci && npm run build && npm run start -- -p $PORT -H 0.0.0.0Это честно, но медленно, потому что сборка выполняется при каждом запуске. Лучше собирать, когда меняется код, а не когда стартует процесс: собирайте в CI, коммитьте или загружайте результат, а командой запуска пусть будет npm ci --omit=dev && npm run start.
Подключите репозиторий вместо загрузки файлов. На RE:NODE интеграция с Git работает только с GitHub, через GitHub App с короткоживущими токенами, чтобы работали приватные репозитории, и с двумя переключателями: подтягивать ветку при каждом запуске и деплоить при push, что перезапускает уже работавший сервер. Одна запись на каждый деплой показывает, какой именно push сейчас действует. Пошаговый разбор есть в статье деплой приложения Node.js из GitHub.
Деплой стоит провала: процесс останавливается, новый собирается или запускается, а пока он не слушает, прокси не с кем разговаривать. С одним процессом свести этот провал к нулю нельзя, можно лишь сделать его коротким. Собирайте заранее, деплойте при низком трафике и прочитайте статью деплой без простоя на сервере, где всего по одному, чтобы узнать, что действительно помогает. Если ваше приложение Next ещё и открывает маршруты API, от которых зависят другие системы, эксплуатационные советы из статьи деплой Express API в production - таймауты, корректное завершение, health-эндпоинты - применимы без изменений.
Диагностика#
`Could not find a production build in the '.next' directory`. Вы запустили без сборки, или результат сборки не попал в то, что было развёрнуто. .next обычно лежит в .gitignore, так что получение репозитория его не приносит.
Сборка убивается без ошибки, код выхода 137. Контейнер упёрся в лимит памяти во время сборки. Собирайте в другом месте или на более крупном тарифе.
Сайт загружается без стилей. Standalone-сборка без скопированных public и .next/static.
`EADDRINUSE` при запуске. Предыдущий процесс всё ещё держит порт. Остановите его как следует; в панели используйте Stop, а не запускайте вторую копию.
Страница не обновляется. Выясните, какой это кэш. Статически сгенерированной странице нужна ревалидация или пересборка; результат fetch может кэшироваться по умолчаниям вашей версии Next; браузер может её держать. Проверяйте в этом порядке.
Переменная окружения не действует. Если она начинается с NEXT_PUBLIC_, она была вшита в бандл при сборке, и нужна пересборка.
Изображения дают 404 или ошибку имени хоста. images.remotePatterns не перечисляет источник.
FAQ#
Нужна ли конкретная платформа для запуска Next.js?
Нет. next build и next start - поддерживаемый способ self-hosting, и процесс Node за обратным прокси запускает фреймворк так, как задумано. Вы берёте на себя эксплуатационную сторону: сборки, перезапуски, каталог кэша и сертификат, - об этом и вся остальная статья.
Почему сборке не хватает памяти, если приложение использует всего 200 МБ?
Потому что компиляция, проверка типов и бандлинг выполняются одновременно и удерживают весь граф модулей. Работающее приложение держит из этого почти ничего. Рассчитывайте размер под сборку или вынесите сборку с сервера.
Стоит ли использовать standalone-вывод?
Используйте, если мало места на диске или времени на установку, либо если вы собираете образ для доставки. Не забудьте скопировать public и .next/static рядом со сгенерированным server.js и явно задайте HOSTNAME и PORT.
Можно ли хостить сайт на Next.js на статическом тарифе или тарифе PHP?
Только если экспортировать его. output: "export" даёт обычные HTML, CSS и JavaScript без сервера, которые раздаст любой статический хостинг, но при этом теряются серверный рендеринг, ISR, обработчики маршрутов и оптимизация изображений. Если ваш сайт - контент, меняющийся на этапе сборки, такой компромисс часто хорош; это разбирают статьи хостинг статических сайтов и веб-хостинг.
Сколько памяти нужно серверу Next.js во время работы?
Небольшому сайту после сборки комфортно в 512 МБ - 1 ГБ, а потребление растёт с конкурентностью, размером таблицы маршрутов и кэшем изображений. Пик - это сборка, а не обслуживание. Понаблюдайте за графиком памяти в консоли сутки, прежде чем решать, и прочитайте статью как рассчитать размер веб-приложения к дню запуска, если ждёте всплеск.
Почему страница устарела после изменения контента?
Потому что что-то намеренно её закэшировало. Выясните, статически ли сгенерирована страница и ждёт окна ревалидации, закэширован ли fetch или дело в браузере. Ревалидация по требованию через revalidatePath после изменения контента убирает гадание.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.