В стандартной модели развертывания Node.js-приложений образ контейнера или серверная среда включает в себя полную директорию node_modules. Для крупных корпоративных Next.js-проектов вес этой директории часто превышает 800 МБ–1.5 ГБ. В нее попадают не только транзитивные зависимости рантайма, но и сборочные утилиты (TypeScript компилятор, Webpack/Turbopack плагины, PostCSS, ESLint, тестовые библиотеки), которые абсолютно не требуются во время обработки входящих HTTP-запросов в продакшене.
Развертывание такого объема данных влечет за собой ряд фундаментальных инфраструктурных проблем:
Для решения этих задач команда Next.js разработала режим изолированной компиляции: output: 'standalone'.
[Исходный проект: 1.2 GB]
├── node_modules/ (все зависимости: prod + dev)
├── src/
└── next.config.ts (output: 'standalone')
│
▼ [next build]
┌─────────────────────────┐
│ @vercel/nft AST-анализ │ ──► Трассировка графа импортов
└─────────────────────────┘
│
▼
[.next/standalone: ~65 MB]
├── node_modules/ ──► ТОЛЬКО реально импортированные JS-файлы
├── package.json
├── server.js ──► Кастомный легковесный HTTP-сервер Node.js
└── .next/server/ ──► Скомпилированные RSC, Route Handlers и страницы
[Изолированные статические ассеты] (НЕ входят в standalone)
├── public/ ──► Загружаются напрямую в CDN / Object Storage
└── .next/static/ ──► Хэшированные клиентские JS/CSS чанкиПод капотом режима standalone лежит специализированный движок трассировки файлов: @vercel/nft (Node File Trace). Во время фазы next build компилятор не просто копирует манифест package.json, а выполняет глубокий статический обход дерева AST для каждой страницы, серверного компонента (RSC) и API Route Handler.
Движок детектирует все формы вызовов модульной системы:
import { x } from 'pkg').import('pkg')).require('pkg')).path.join(__dirname, 'data.json') и fs.readFileSync(...).В результате в директорию .next/standalone копируются не пакеты целиком, а исключительно те конкретные .js и .json файлы из node_modules, к которым фактически происходит обращение. Все devDependencies, а также неиспользуемый код (dead code) и документация пакетов полностью отсекаются.
Ментальная модель output: 'standalone' базируется на концепции герметичного рантайм-среза (Hermetic Runtime Slice) с явным архитектурным разделением динамического вычисления и статической доставки контента.
Next.js разделяет артефакты компиляции на две строго изолированные категории:
.next/standalone): самодостаточный серверный бандл, содержащий сгенерированный входной файл server.js и минимальный срез зависимостей. Для его запуска не требуются установленные глобально или локально CLI-утилиты Next.js. Он запускается командой node server.js через чистый бинарник Node.js.public/ и .next/static/): клиентские бандлы, стили, шрифты и публичные файлы.Входящий сетевой трафик
│
▼
┌─────────────────────────────────┐
│ Edge Ingress / Nginx │
└─────────────────────────────────┘
│ │
│ /_next/static/* │ Динамические маршруты
│ /favicon.ico, /images/* │ SSR / RSC / Server Actions
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────┐
│ CDN / S3 Bucket / │ │ Next.js Standalone Pod │
│ Nginx Static Cache │ │ (node server.js) │
│ │ │ │
│ • Максимальный TTL │ │ • Минимальный оверхед I/O │
│ • Сжатие gzip/brotli │ │ • Изолированный пул памяти │
│ • Нулевая нагрузка │ │ • Отсутствие CLI Next.js │
│ на процессор Node │ │ • Быстрый graceful restart │
└──────────────────────┘ └──────────────────────────────┘Next.js намеренно не копирует папки public/ и .next/static/ внутрь .next/standalone. Это сделано сознательно, чтобы исключить антипаттерн обслуживания тяжелой статики через однопоточный Event Loop Node.js. В высоконагруженной архитектуре статика должна отдаваться напрямую через Nginx, Traefik, AWS CloudFront или S3, разгружая Node.js для вычислений SSR и Server Actions.
Однако при автономном self-hosting в контейнере Docker эти директории необходимо явно скопировать в финальный слой, иначе клиентский роутер получит 404 на файлы манифестов и стилей.
Установка флага output: 'standalone' и параметра outputFileTracingRoot в next.config.ts.
Запуск pnpm build. Движок @vercel/nft производит AST-анализ всех маршрутов и изолирует файлы рантайма в .next/standalone.
Копирование сгенерированного .next/standalone, а также ручной перенос директорий public/ и .next/static/ в runner-образ.
Назначение прав пользователю nextjs:nodejs (UID 1001) и запуск процесса через команду node server.js.
Почему при использовании output: 'standalone' контейнер запускается командой node server.js, а не pnpm start или next start?
В микросервисной архитектуре, где сотни подов Next.js динамически масштабируются по метрикам CPU/Memory, уменьшение размера контейнерного образа с 1.2 ГБ до 85 МБ снижает задержку развертывания нового пода (Image Pull Duration) с десятков секунд до сотен миллисекунд. Это предотвращает потерю входящего трафика при внезапных всплесках нагрузки.
Корпоративные регламенты безопасности требуют сканирования каждого контейнера перед выкладкой в реестр (Registry Scanning). Standalone-сборка отсекает компиляторы, генераторы типов и тестовые фреймворки, сводя количество ложных срабатываний и реальных уязвимостей CVE в node_modules к строгому минимуму.
Когда в едином репозитории находится 20+ пакетов и приложений, обычный node_modules в корне монорепо весит десятки гигабайт. Механизм outputFileTracingRoot собирает только транзитивные зависимости конкретного целевого веб-приложения из всех локальных workspace-пакетов, генерируя изолированный артефакт для развертывания.
Процесс сборки и трассировки зависимостей проходит через строго детерминированный конвейер:
Транспиляция TypeScript, сборка Server Components, Server Actions и клиентских чанков в директорию .next.
Парсинг скомпилированных JS-файлов, выявление статических и динамических путей импорта, разрешение симлинков pnpm.
Физическое копирование выявленных файлов зависимостей в .next/standalone/node_modules с сохранением относительных путей.
Создание легковесного файла server.js, настраивающего HTTP-сервер, роутинг, кэш-хэндлер и проксирование middleware.
Для включения режима и правильной работы в контексте монорепозитория или контейнера настроим next.config.ts:
import type { NextConfig } from 'next';
import path from 'node:path';
const nextConfig: NextConfig = {
// Активация генерации изолированного бандла .next/standalone
output: 'standalone',
// Критично для монорепозиториев: указывает корень проекта,
// относительно которого @vercel/nft вычисляет пути зависимостей
outputFileTracingRoot: path.join(__dirname, '../../'),
// Тонкая настройка включения динамических ассетов,
// которые AST-анализатор не может обнаружить статически
experimental: {
outputFileTracingIncludes: {
// Пример: принудительное включение бинарников Sharp или файлов шаблонов
'/api/generate-pdf': ['./templates/**/*', './fonts/**/*'],
},
outputFileTracingExcludes: {
// Исключение тяжелых пакетов, попавших по ошибке
'*': [
'node_modules/@swc/core-linux-x64-gnu',
'node_modules/@swc/core-linux-x64-musl',
'node_modules/@esbuild/**/*',
],
},
},
// Оптимизация сжатия ответов на уровне Node.js
// (рекомендуется отключать, если сжатие выполняет внешний Nginx/Cloudflare)
compress: false,
// Отключение заголовка x-powered-by для маскировки стека
poweredByHeader: false,
};
export default nextConfig;.next/standalone#После выполнения команды pnpm build формируется следующая файловая иерархия:
.next/standalone/
├── apps/
│ └── web/
│ ├── .next/
│ │ ├── BUILD_ID
│ │ ├── prerender-manifest.json
│ │ ├── routes-manifest.json
│ │ └── server/ <-- RSC пейлоады и серверные функции
│ ├── package.json
│ └── server.js <-- Главная точка входа Node.js
└── node_modules/ <-- Минимальный набор библиотек (React, Next core)Обратите внимание: если проект находится внутри монорепозитория (например, apps/web), то внутри .next/standalone воспроизводится относительная структура папок монорепозитория. Это необходимо для того, чтобы симлинки на зависимости в общем корневом node_modules оставались валидными.
# syntax=docker/dockerfile:1.7
# ── 1. БАЗОВЫЙ ОБРАЗ ──
FROM node:22-alpine AS base
ENV NODE_ENV=production
WORKDIR /app
# ── 2. УСТАНОВКА СБОРОЧНЫХ ЗАВИСИМОСТЕЙ ──
FROM base AS deps
# Установка libc6-compat для корректной работы Turbopack на Alpine Linux
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
# Активация corepack и установка строго залоченных зависимостей
RUN corepack enable pnpm && \
pnpm install --frozen-lockfile --prod=false
# ── 3. СБОРКА АРТЕФАКТОВ ──
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Отключение телеметрии во время компиляции
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN corepack enable pnpm && \
pnpm build
# ── 4. ФИНАЛЬНЫЙ РАНТАЙМ ──
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV NEXT_TELEMETRY_DISABLED=1
# Создание непривилегированного системного пользователя
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# КРИТИЧЕСКИЙ ШАГ: Копирование статических ассетов,
# которые намеренно не входят в .next/standalone
COPY --from=builder /app/public ./public
# Установка правильных прав владения файлами
RUN mkdir .next && chown nextjs:nodejs .next
# Копирование изолированного серверного бандла
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# Копирование клиентских кэшируемых чанков внутрь структуры .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Переключение на non-root пользователя
USER nextjs
EXPOSE 3000
# Запуск сгенерированного сервера напрямую через Node.js (без CLI next start)
CMD ["node", "server.js"]С какой целью Next.js намеренно исключает директории public/ и .next/static/ из папки .next/standalone?
Разработчик включает output: 'standalone', собирает Docker-образ, копирует только .next/standalone и запускает контейнер. Приложение открывается в браузере как сырой HTML без стилей, а в консоли возникают ошибки 404 Not Found на файлы /_next/static/chunks/....
Почему так делают: разработчики предполагают, что папка standalone содержит в себе абсолютно все файлы, необходимые для полноценной работы сайта, по аналогии с обычным build.
Что происходит в продакшене: сервер server.js ожидает, что клиентские статические файлы расположены по относительному пути ./.next/static. Если их не скопировать из этапа сборщика явно, клиентская гидратация полностью ломается.
# ❌ ОШИБКА: Скопирован только standalone
COPY --from=builder /app/.next/standalone ./
# ✅ ПРАВИЛЬНО: Обязательное явное копирование static и public
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/staticВ монорепозитории Turborepo при сборке standalone приложения apps/web генератор не включает зависимости, объявленные в общем корне проекта или соседних пакетах (packages/ui), что приводит к фатальной ошибке MODULE_NOT_FOUND при запуске node server.js.
Почему так делают: полагаются на стандартные настройки Next.js, который по умолчанию считает корнем проекта текущую рабочую директорию пакета apps/web.
Что происходит в продакшене: алгоритм @vercel/nft обрезает дерево файлов на уровне apps/web и не выходит выше, игнорируя корневой node_modules и симлинки pnpm.
// ❌ ОШИБКА: Отсутствует указание корня монорепозитория
const nextConfig: NextConfig = {
output: 'standalone',
};
// ✅ ПРАВИЛЬНО: Явное указание корня монорепо
import path from 'node:path';
const nextConfig: NextConfig = {
output: 'standalone',
outputFileTracingRoot: path.join(__dirname, '../../'),
};В коде серверного компонента или Route Handler выполняется динамическое чтение файла с диска: fs.readFileSync(path.join(process.cwd(), 'locales', lang + '.json')). При локальной разработке все работает, но в контейнере standalone код падает с ошибкой ENOENT: no such file or directory.
Почему так делают: разработчики забывают, что @vercel/nft работает через статический анализ кода. Динамическая конкатенация строк lang + '.json' не позволяет анализатору вычислить конкретные имена файлов во время сборки.
Что происходит в продакшене: файлы локализации не включаются в .next/standalone и физически отсутствуют в контейнере.
// next.config.ts
// ✅ ПРАВИЛЬНО: Декларативное указание файлов для включения в бандл
const nextConfig: NextConfig = {
output: 'standalone',
experimental: {
outputFileTracingIncludes: {
'/api/i18n/**': ['./locales/**/*'],
},
},
};В манифесте Dockerfile в инструкции CMD указывается ["pnpm", "start"] или ["next", "start"].
Почему так делают: сохраняют старые привычки развертывания стандартного Next.js-приложения.
Что происходит в продакшене: для запуска next start требуется присутствие бинарников CLI Next.js в node_modules/.bin, которые были отсечены сборщиком standalone. Контейнер аварийно завершается с кодом command not found: next. В режиме standalone точкой входа является исключительно сгенерированный скрипт server.js.
# ❌ АНТИПАТТЕРН: Вызов CLI next
CMD ["pnpm", "start"]
# ✅ ПРАВИЛЬНО: Прямой запуск скомпилированного entrypoint
CMD ["node", "server.js"]