Архитектурная эволюция: сопоставление PM2, автономного режима Standalone и контейнерных инициализаторов
Исторически развертывание приложений на базе Node.js на физических серверах или виртуальных машинах (VPS) опиралось на менеджеры процессов, среди которых стандартом де-факто стал PM2. В традиционных операционных системах PM2 решал фундаментальные задачи однопоточной природы Node.js: перезапуск при сбоях, автоматический запуск после перезагрузки ОС, ротацию журналов и вертикальное масштабирование на все доступные ядра центрального процессора посредством модуля cluster среды выполнения. Однако фундаментальный сдвиг в сторону микросервисной и контейнерной архитектуры видоизменил распределение ответственности между компонентами инфраструктуры.
В экосистеме Docker базовой парадигмой является изоляция одного процесса в рамках одного контейнера. Попытка интеграции PM2 внутрь Docker-контейнера создает избыточный архитектурный слой — супервизор над другим супервизором. Демон Docker вместе с оркестраторами (Docker Compose, Kubernetes, Nomad) самостоятельно отслеживает жизненный цикл контейнеров, реализует политики перезапуска (restart: unless-stopped) и собирает метрики. Если внутри контейнера запускается стандартный демон PM2, дочерний процесс Node.js может аварийно завершиться или зависнуть в цикле утечки памяти, но для внешнего оркестратора контейнер продолжит отображаться как работоспособный, поскольку процесс самого менеджера PM2 остается активным.
Начиная с версии 12, фреймворк Next.js предлагает встроенный режим сборки output: "standalone". Данный механизм задействует алгоритм трассировки файлов зависимостей (@vercel/nft), сканирует дерево импортов и генерирует минимальный рабочий каталог .next/standalone. В него помещаются исключительно используемые в продакшене модули и скомпилированный HTTP-сервер server.js. Это позволяет отказаться от громоздкой команды next start, требующей наличия всей исходной директории node_modules, и сократить размер итогового образа Docker с 1–1.5 ГБ до компактных 100–150 МБ.
Архитектурным барьером для использования PM2 в режиме кластера (exec_mode: 'cluster') внутри контейнера является внутренняя модель кэширования Next.js. Архитектура App Router глубоко полагается на инкрементную статическую регенерацию (ISR), серверный кэш запросов (fetch cache) и кэш компонентов. В стандартной конфигурации кэш размещается в оперативной памяти или локальной файловой системе конкретного процесса. При запуске нескольких инстансов под управлением PM2 каждый рабочий процесс формирует изолированный сегмент кэша. Когда входящий вебхук или серверное действие (Server Action) вызывает инвалидацию по тегу (revalidateTag), сброс данных происходит только в памяти одного воркера, в то время как остальные продолжают отдавать устаревшие данные. Масштабирование Next.js как внутри многопроцессного контейнера, так и между несколькими репликами требует выноса состояния во внешнее распределенное хранилище (например, Redis) с реализацией интерфейса cacheHandler и установкой параметра cacheMaxMemorySize: 0.
Еще одной технической проблемой прямого запуска Node.js в контейнерах является поведение процесса с идентификатором PID 1. Среда Node.js изначально не проектировалась для выполнения функций системного процесса инициализации: она не пересылает POSIX-сигналы (SIGTERM, SIGINT) дочерним процессам и не очищает таблицу процессов от завершившихся дочерних задач, что провоцирует накопление «зомби-процессов» (defunct) и приводит к десятисекундным задержкам остановки контейнера по таймауту SIGKILL.
Для решения проблемы PID 1 современная практика контейнеризации предписывает использовать не тяжеловесный менеджер PM2, а специализированные инициализаторы вроде Tini. Платформа Docker Compose предоставляет встроенную поддержку Tini через директиву init: true, что исключает необходимость добавления вспомогательных бинарников в дистрибутив контейнера. Если специфика проекта безальтернативно требует присутствия PM2, запуск в контейнере допустим исключительно с помощью утилиты pm2-runtime, удерживающей процесс на переднем плане (foreground), преимущественно в режиме fork для одного инстанса.
Характеристика
Standalone + Docker Compose (Рекомендуемый подход)
PM2 Runtime внутри Docker (pm2-runtime)
Классический сервер VPS с PM2
Управление жизненным циклом
Демон Docker (restart_policy, healthcheck)
pm2-runtime внутри контейнера
Системный демон PM2 и служба Systemd
Размер дискового пространства
Минимальный (100–150 МБ за счет трассировки файлов)
Средний (250–450 МБ из-за глобального PM2)
Большой (1–2 ГБ, полный каталог node_modules)
Обработка сигналов ядра (PID 1)
Аппаратная через init: true (интегрированный Tini)
Встроена в исполняемый файл pm2-runtime
Переложена на системный менеджер ОС (Systemd)
Горизонтальное масштабирование
Репликация контейнеров через scale и балансировщик
Запуск дочерних процессов (exec_mode: 'cluster')
Внутренний кластер PM2 на ядра CPU
Координация кэша ISR
Требует внешний Redis при числе реплик более одной
Требует внешний Redis даже внутри одного контейнера
Требует внешний Redis при кластеризации
Мониторинг доступности
Внешний опрос healthcheck через API оркестратора
Локальные модули PM2 или интерфейс PM2 Plus
Утилиты командной строки (pm2 monit, pm2 logs)
Структурная организация проекта и топология каталогов
Четкое разделение конфигураций инфраструктуры и исходного кода приложения обеспечивает предсказуемость сборки и устраняет риск утечки служебных файлов в продуктивную среду. Рекомендуется придерживаться модульной топологии, где декларации сред разработки и эксплуатации разнесены по специализированным файлам Compose и Dockerfile.
Структура проекта организуется следующим образом:
-
my-nextjs-app/— корневая директория репозитория приложения.-
.dockerignore— декларация исключений из контекста сборки Docker-образов. -
.gitignore— фильтрация служебных файлов для системы контроля версий Git. -
.env.example— эталонный шаблон переменных окружения без чувствительных данных. -
.env.production.example— шаблон переменных производственной среды. -
compose.dev.yaml— оркестрация локального окружения с поддержкой горячей перезагрузки. -
compose.prod.yaml— конфигурация оркестрации продуктивного контура с изоляцией сетей. -
Dockerfile— многоэтапный сборочный манифест для формирования минимального production-образа. -
Dockerfile.dev— конфигурация контейнера разработки с полным набором инструментов отладки. -
ecosystem.config.js— конфигурационный манифест PM2 (применяется при регламентном требовании запуска через супервизор). -
next.config.ts— конфигурация фреймворка с активацией автономного выводаstandalone. -
package.jsonиpackage-lock.json— манифесты зависимостей и фиксации версий пакетов. -
public/— статически обслуживаемые ресурсы фронтенда (изображения, шрифты, фавиконы). -
src/— основной каталог исходного кода.-
app/— маршрутизация и страницы App Router.-
api/health/route.ts— контроллер проверки жизнеспособности сервиса. -
layout.tsx— корневой макет интерфейса. -
page.tsx— главная страница приложения.
-
-
-
deploy/— инфраструктурные скрипты и файлы конфигураций серверов.-
nginx/conf.d/default.conf— параметры обратного прокси с оптимизацией под потоковый рендеринг Next.js.
-
-
tsconfig.json— конфигурация компилятора TypeScript.
-
Подобная архитектура четко разграничивает среду быстрой локальной итерации разработчика и жестко зафиксированный продуктивный контур, предотвращая случайную подмену боевых образов конфигурациями с активным HMR-сервером.
Политика изоляции артефактов: .gitignore и .dockerignore
Корректное составление файлов фильтрации исключает попадание в репозиторий конфиденциальных параметров, а также блокирует отправку лишних мегабайт данных демону сборки BuildKit, оптимизируя время создания слоев.
Критическая особенность компилятора Next.js заключается в обработке переменных среды при сборке standalone: если файлы .env присутствуют в контексте билда, утилита next build запекает их содержимое в артефакты каталога .next/standalone, после чего секреты беспрепятственно утекают в публикуемый образ. По этой причине локальные конфигурации окружения должны быть категорически изолированы от сборочного контекста Docker.
Ниже представлена конфигурация .gitignore:
# Зависимости среды выполненияnode_modules/.pnp.pnp.js# Артефакты сборки и кэши Next.js.next/out/build/dist/# Отладочные журналыnpm-debug.log*yarn-debug.log*yarn-error.log*.pnpm-debug.log*# Конфиденциальные переменные окружения.env.env*.local!.env.example!.env.production.example# Кэш сборочных конвейеров.turbo/coverage/# Системные метаданные.DS_Store*.pem
Файл .dockerignore строится на базе принципа жесткого отсечения локальных модулей, систем контроля версий и конфигурационных файлов хоста, чтобы гарантировать установку нативных бинарников (например, модуля манипуляции изображениями sharp) строго под целевую архитектуру Linux-контейнера:
# Локальные модули и артефакты (создаются изолированно внутри контейнера)node_modules.nextoutbuilddist.turbo# Система контроля версий и метаданные.git.gitignore.github.gitlabREADME.md# Локальные файлы окружения (секреты передаются на этапе запуска).env.env*.local.env.development.env.test# Файлы декларации контейнеровDockerfile*compose*.yamldocker-compose*.yml.dockerignore# Временные системные файлы*.log.npm.yarn
Проектирование сборочных образов: Dockerfile для Development и Production
Для локальной разработки критически важна скорость внесения правок и доступность всех инструментов сборки, тогда как для рабочей среды приоритетом становятся неизменяемость, защищенность и компактность.
Среда разработки: Dockerfile.dev
Манифест Dockerfile.dev ориентирован на установку полного дерева зависимостей (включая devDependencies) и поддержание соединения горячей замены модулей (HMR). Системная библиотека libc6-compat подключается для корректной интеграции низкоуровневых бинарных модулей в дистрибутивах на базе Alpine Linux.
Dockerfile
FROM node:22-alpineWORKDIR /app# Подключение базовых библиотек совместимости C-рантаймаRUN apk add --no-cache libc6-compat# Кэширование слоя зависимостейCOPY package.json package-lock.json* ./RUN npm install# Копирование исходного кода (при монтировании томов перекрывается хостом)COPY . .ENV PORT=3000ENV HOSTNAME="0.0.0.0"ENV NODE_ENV=developmentEXPOSE 3000CMD ["npm", "run", "dev"]
Среда эксплуатации: Production Dockerfile (Multi-Stage)
Многоэтапная сборка изолирует компиляцию приложения от финального рантайма. В первом этапе (dependencies) выполняется чистая установка пакетов с использованием кэширования BuildKit (--mount=type=cache).
Во втором этапе (builder) компилируется проект.
Финальный этап (runner) содержит только необходимые файлы.
Важнейшая деталь автономного режима Next.js состоит в том, что статический каталог .next/static и общедоступная папка public/ намеренно не упаковываются в .next/standalone/, поскольку авторы фреймворка рассчитывают на их выгрузку в CDN. При автономном запуске контейнера эти каталоги необходимо скопировать вручную по строго определенным путям, иначе браузер получит HTML-разметку без стилей и изображений.
Кроме того, по соображениям информационной безопасности запуск процесса осуществляется от лица непривилегированного пользователя nextjs с фиксированным идентификатором UID 1001.
Dockerfile
ARG NODE_VERSION=22-alpineFROM node:${NODE_VERSION} AS baseRUN apk add --no-cache libc6-compatWORKDIR /app# Этап 1: Разрешение и изоляция зависимостейFROM base AS dependenciesCOPY package.json package-lock.json* ./RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund# Этап 2: Компиляция приложенияFROM base AS builderCOPY --from=dependencies /app/node_modules ./node_modulesCOPY . .ENV NEXT_TELEMETRY_DISABLED=1ENV NODE_ENV=production# Сборка standalone-артефактов с кэшированием сборщикаRUN --mount=type=cache,target=/app/.next/cache npm run build# Этап 3: Минимальный продакшен-образFROM node:${NODE_VERSION} AS runnerWORKDIR /appENV NODE_ENV=productionENV NEXT_TELEMETRY_DISABLED=1ENV PORT=3000ENV HOSTNAME="0.0.0.0"# Ограничение привилегий: запуск из-под системного пользователяRUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs# Утилита проверки жизнеспособности контейнераRUN apk add --no-cache curl# Копирование статических ресурсов фронтендаCOPY --from=builder --chown=nextjs:nodejs /app/public ./public# Перенос сгенерированного автономного сервераCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./# Перенос клиентских хэшированных чанков в ожидаемую структуру каталоговCOPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static# Предоставление прав на директорию динамического кэшаRUN mkdir -p .next && chown -R nextjs:nodejs .nextUSER nextjsEXPOSE 3000# Запуск автономного сервера напрямую через среду Node.jsCMD ["node", "server.js"]
Декларативная оркестрация: Docker Compose для Dev и Prod
Для управления конфигурациями контейнеров используются независимые манифесты Compose, отражающие специфику каждой среды.
Манифест разработки: compose.dev.yaml
В среде разработки критично исключить перезапись каталога /app/node_modules контейнера модулями хост-машины. Для этого применяется теневой анонимный том (anonymous volume).
Переменная WATCHPACK_POLLING=true активирует опрос файловой системы механизмом отслеживания изменений, что устраняет проблемы передачи событий через виртуализированные файловые дескрипторы под Windows (WSL2) и macOS.
services: web-dev: build: context: . dockerfile: Dockerfile.dev container_name: nextjs_dev_container ports: - "3000:3000" environment: - NODE_ENV=development - WATCHPACK_POLLING=true - NEXT_TELEMETRY_DISABLED=1 volumes: # Проброс исходного кода в реальном времени - .:/app # Изоляция зависимостей контейнера от хоста - /app/node_modules # Изоляция локальных кэшей компилятора - /app/.next networks: - dev_networknetworks: dev_network: driver: bridge
Запуск осуществляется вызовом:
docker compose -f compose.dev.yaml up --build
Продакшен-оркестрация: compose.prod.yaml
В боевом окружении сервис Next.js закрывается обратным прокси-сервером Nginx. Ключевой настройкой является директива init: true, которая делегирует Tini обработку системных сигналов ядра, гарантируя корректное завершение открытых сетевых соединений при обновлении версий.
Директива healthcheck выполняет активный мониторинг состояния через внутренний эндпоинт, предотвращая подачу пользовательского трафика на неинициализированный узел.
services: web: build: context: . dockerfile: Dockerfile image: nextjs-standalone-app:latest container_name: nextjs_app_prod restart: unless-stopped # Инициализация Tini в качестве PID 1 для обработки сигналов и зомби init: true stop_grace_period: 30s environment: - NODE_ENV=production - PORT=3000 - HOSTNAME=0.0.0.0 - DATABASE_URL=${DATABASE_URL} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} deploy: resources: limits: cpus: "2.0" memory: 2048M reservations: cpus: "0.5" memory: 512M healthcheck: test: ["CMD-SHELL", "curl -f http://127.0.0.1:3000/api/health || exit 1"] interval: 15s timeout: 5s retries: 3 start_period: 20s networks: - app_network nginx: image: nginx:1.27-alpine container_name: nextjs_proxy_prod restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./deploy/nginx/conf.d:/etc/nginx/conf.d:ro depends_on: web: condition: service_healthy networks: - app_networknetworks: app_network: driver: bridge
Запуск продуктивного комплекса выполняется следующей командой:
docker compose -f compose.prod.yaml up -d --build
Сопутствующие компоненты производственного контура
Конфигурация фреймворка: next.config.ts
Для активации генерации самодостаточного сервера в конфигурацию Next.js добавляется параметр output: "standalone". Дополнительно отключается заголовок идентификации платформы x-powered-by для снижения информативности потенциальных векторов атак.
import type { NextConfig } from "next";const nextConfig: NextConfig = { output: "standalone", reactStrictMode: true, poweredByHeader: false, compress: true,};export default nextConfig;
Реализация контроллера проверки состояния: src/app/api/health/route.ts
Вспомогательный маршрут проверки работоспособности информирует оркестратор о текущем состоянии экземпляра приложения. Объявление маршрута как force-dynamic исключает его попадание в статический кэш.
import { NextResponse } from "next/server";export const dynamic = "force-dynamic";export async function GET() { try { const healthData = { status: "healthy", timestamp: new Date().toISOString(), uptime: process.uptime(), memoryRssBytes: process.memoryUsage().rss, }; return NextResponse.json(healthData, { status: 200 }); } catch (error) { return NextResponse.json( { status: "unhealthy", message: (error as Error).message }, { status: 503 } ); }}
Альтернативная интеграция с PM2: ecosystem.config.js
Если внутренние стандарты предприятия предписывают обязательное внедрение PM2, в сборочный образ стадии runner устанавливается утилита PM2 (RUN npm install -g pm2), а запуск транслируется на команду CMD ["pm2-runtime", "start", "ecosystem.config.js"].
Файл ecosystem.config.js в корневой директории направляется непосредственно на автономный входной файл server.js:
module.exports = { apps: [ { name: "nextjs-app", script: "./server.js", // Принудительный режим fork во избежание рассинхронизации локального ISR-кэша instances: 1, exec_mode: "fork", max_memory_restart: "1500M", env: { NODE_ENV: "production", PORT: 3000, }, }, ],};
Конфигурация Nginx с поддержкой потоковой передачи данных
Современный стек Next.js (начиная с версий 13–15+) в режиме App Router опирается на компонентную модель React Server Components, где интерфейс передается потоково через чанкирование (Transfer-Encoding: chunked).
Стандартная конфигурация прокси-буферизации Nginx удерживает эти пакеты до полного формирования страницы, полностью разрушая пользовательский опыт от использования компонентов loading.tsx и границ Suspense.
В файле deploy/nginx/conf.d/default.conf буферизация принудительно отключается директивами proxy_buffering off; и proxy_set_header X-Accel-Buffering no;:
upstream nextjs_backend { server web:3000; keepalive 32;}server { listen 80; server_name localhost; server_tokens off; client_max_body_size 25M; gzip on; gzip_proxied any; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; location / { proxy_pass http://nextjs_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Критическая секция: обеспечение работоспособности Streaming SSR в App Router proxy_buffering off; proxy_request_buffering off; proxy_set_header X-Accel-Buffering no; proxy_read_timeout 60s; proxy_connect_timeout 60s; } # Долгосрочное кэширование неизменяемых хэшированных файлов Next.js location /_next/static/ { proxy_pass http://nextjs_backend; proxy_http_version 1.1; proxy_set_header Host $host; add_header Cache-Control "public, max-age=31536000, immutable"; }}
Синтез архитектурных решений
Переход от монолитных серверов с локальными демонами PM2 к контейнеризированным системам меняет баланс ответственности: управление процессами полностью делегируется уровню оркестрации.
Внедрение легковесного инициализатора Tini через init: true в Docker Compose закрывает риски зависания сигналов и накопления зомби-процессов без раздувания кодовой базы сторонними библиотеками управления потоками.
Использование автономного режима output: "standalone" обеспечивает минимальный размер образов и гарантирует изоляцию сборочных секретов при условии строгой настройки файлов .dockerignore.
Масштабирование системы осуществляется горизонтальным размножением контейнеров с балансировкой через Nginx, где обязательное отключение прокси-буферизации сохраняет реактивность потокового рендеринга современного Next.js. При расширении кластера сверх одного контейнера обязательным условием консистентности системы становится перевод внутреннего кэша фреймворка на единое внешнее хранилище типа Redis.
Источник: habr.com