У стандартній моделі розгортання 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 / S3
└── .next/static/ ──► Хешовані клієнтські JS/CSS чанкиПід капотом режиму standalone функціонує спеціалізований рушій трасування файлів: @vercel/nft (Node File Trace). Під час фази next build компілятор виконує глибинний статичний обхід AST-дерева для кожного серверного компонента (RSC), оболонки сторінки та 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 │
│ ження на CPU 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 під час завантаження маніфестів і чанків.
Ключові концепції standalone-деплою:
У мікросервісних інфраструктурах, де сотні подів Next.js динамічно масштабуються за показниками CPU/Memory, зменшення розміру контейнерного образу з 1.2 ГБ до 85 МБ скорочує затримку завантаження нового поду (Image Pull Duration) із 45 секунд до сотень мілісекунд. Це захищає систему від втрати запитів під час різких сплесків трафіку.
Регламенти безпеки вимагають автоматичного сканування кожного контейнера перед завантаженням у реєстр (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-сервер, роутинг та адаптери кешування.
Для активації режиму та коректної роботи в монорепозиторії або контейнері налаштуємо 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: {
// Приклад: примусове включення шаблонів генерації PDF або бінарних шрифтів
'/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/ <-- Мінімальний набір залежностейЗверніть увагу: якщо проєкт знаходиться всередині робочого простору монорепозиторію (наприклад, 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
# Створення папки .next із правильними правами власності
RUN mkdir .next && chown nextjs:nodejs .next
# Копіювання ізольованого серверного бандла
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# Копіювання клієнтських кешованих чанків у .next/static
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"]Розробник вмикає output: 'standalone', збирає Docker-образ, копіюючи лише .next/standalone, і запускає контейнер. Застосунок відкривається у браузері без стилів, а консоль фіксує каскадні помилки 404 Not Found на файли /_next/static/chunks/....
Причина: Розробники помилково вважають, що каталог standalone автоматично містить абсолютно всі клієнтські ресурси.
Реальність у продакшені: Сервер 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 під час збірки застосунку 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, '../../'),
};У коді серверного компонента викликається динамічне читання файлу: fs.readFileSync(path.join(process.cwd(), 'locales', lang + '.json')). Під час локальної розробки все працює, але в контейнері standalone процес падає з помилкою ENOENT: no such file or directory.
Причина: @vercel/nft працює через статичний AST-аналіз. Динамічна конкатенація рядків (lang + '.json') не дає змоги аналізатору визначити точні імена файлів під час компіляції.
Реальність у продакшені: Файли локалізації не потрапляють до .next/standalone і фізично відсутні в контейнері.
// next.config.ts
// ✅ ПРАВИЛЬНО: Декларативне включення статичних ресурсів у бандл
const nextConfig: NextConfig = {
output: 'standalone',
experimental: {
outputFileTracingIncludes: {
'/api/i18n/**': ['./locales/**/*'],
},
},
};У Dockerfile в інструкції CMD вказується ["pnpm", "start"] або ["next", "start"].
Причина: Збереження старих звичок деплою стандартного застосунку.
Реальність у продакшені: Команда next start вимагає наявності CLI Next.js у node_modules/.bin, які були навмисно видалені під час мінімізації standalone. Контейнер завершується з помилкою command not found: next. Точкою входу в режимі standalone є виключно node server.js.
# ❌ АНТИПАТЕРН: Виклик CLI фреймворку
CMD ["pnpm", "start"]
# ✅ ПРАВИЛЬНО: Прямий запуск скомпільованого вхідного файлу
CMD ["node", "server.js"]Встановлення прапорця 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.