En el modelo tradicional de despliegue de aplicaciones Node.js, la imagen del contenedor o el entorno del servidor incluye la totalidad del directorio node_modules. En proyectos empresariales Next.js de gran escala, este directorio suele oscilar entre 800 MB y 1.5 GB. Dicha carpeta empaqueta no solo las dependencias transitivas de ejecución, sino también herramientas de compilación (el compilador de TypeScript, plugins de Webpack/Turbopack, PostCSS, ESLint y librerías de pruebas), las cuales resultan enteramente superfluas para atender peticiones HTTP en producción.
Desplegar este volumen de datos acarrea serios inconvenientes de infraestructura:
Para resolver este desafío, el equipo de Next.js desarrolló el modo de compilación aislada: output: 'standalone'.
[Proyecto Origen: 1.2 GB]
├── node_modules/ (todas las dependencias: prod + dev)
├── src/
└── next.config.ts (output: 'standalone')
│
▼ [next build]
┌─────────────────────────┐
│ @vercel/nft Análisis AST│ ──► Rastreo del grafo de importaciones
└─────────────────────────┘
│
▼
[.next/standalone: ~65 MB]
├── node_modules/ ──► SOLO archivos JS requeridos en ejecución
├── package.json
├── server.js ──► Servidor HTTP ligero nativo de Node.js
└── .next/server/ ──► RSCs compilados, Route Handlers y páginas
[Activos Estáticos Aislados] (EXCLUIDOS intencionadamente de standalone)
├── public/ ──► Servidos directamente por CDN / Object Storage
└── .next/static/ ──► Chunks JS/CSS inmutables con hashBajo el capó del modo standalone opera un motor especializado de rastreo de archivos: @vercel/nft (Node File Trace). Durante la fase next build, el compilador efectúa un recorrido estático exhaustivo del AST de cada componente de servidor (RSC), shell de página y Route Handler.
El analizador detecta todas las modalidades de invocación de módulos:
import { x } from 'pkg').import('pkg')).require('pkg')).path.join(__dirname, 'data.json') y fs.readFileSync(...).En consecuencia, en .next/standalone no se copian los paquetes completos, sino exclusivamente los archivos .js y .json de node_modules que se ejecutan realmente. Todas las devDependencies, el código muerto (dead code), los tests y la documentación de las librerías son eliminados.
El modelo mental de output: 'standalone' descansa en el principio de corte hermético de ejecución (Hermetic Runtime Slice), con una separación arquitectónica estricta entre la computación dinámica y la entrega estática.
Next.js segrega los artefactos de compilación en dos categorías herméticas:
.next/standalone): Un paquete de servidor autónomo que contiene el archivo de entrada generado server.js y el conjunto mínimo de dependencias. No requiere la instalación global ni local de los comandos CLI de Next.js; se inicia directamente con node server.js sobre el binario base de Node.js.public/ y .next/static/): Paquetes de cliente, hojas de estilo, tipografías y archivos públicos.Tráfico de Red Entrante
│
▼
┌─────────────────────────────────┐
│ Edge Ingress / Nginx │
└─────────────────────────────────┘
│ │
│ /_next/static/* │ Rutas dinámicas
│ /favicon.ico, /images/* │ SSR / RSC / Server Actions
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────┐
│ CDN / S3 Bucket / │ │ Next.js Standalone Pod │
│ Nginx Static Cache │ │ (node server.js) │
│ │ │ │
│ • TTL de caché máx. │ │ • Mínimo coste de I/O │
│ • Compresión Gzip/Br │ │ • Memoria aislada y controlada│
│ • Cero impacto en el │ │ • Sin el CLI de Next.js │
│ hilo de Node.js │ │ • Reinicio graceful rápido │
└──────────────────────┘ └──────────────────────────────┘Next.js no copia intencionadamente las carpetas public/ y .next/static/ dentro de .next/standalone. Servir archivos estáticos pesados mediante el bucle de eventos monohilo de Node.js es un antipatrón en producción. En arquitecturas de alto rendimiento, los activos estáticos deben entregarse directamente desde Nginx, Traefik, AWS CloudFront o S3, preservando los hilos de Node.js para el renderizado SSR y Server Actions.
No obstante, en entornos de autocontenedor Docker autónomos, estas carpetas deben copiarse explícitamente a la capa de ejecución; de lo contrario, el enrutador del cliente recibirá errores 404 al solicitar los manifiestos y estilos.
Conceptos fundamentales del despliegue standalone:
En arquitecturas de microservicios donde cientos de pods de Next.js escalan según métricas de CPU y memoria, reducir la imagen de 1.2 GB a 85 MB minimiza la latencia de descarga (Image Pull Duration) de 45 segundos a unos pocos milisegundos, previniendo la saturación ante picos de demanda.
Las políticas de cumplimiento exigen el escaneo exhaustivo de contenedores antes de su publicación en el registro. La compilación standalone descarta compiladores, suites de pruebas y scripts de desarrollo, reduciendo a mínimos las alertas de vulnerabilidades CVE en node_modules.
Cuando en un mismo repositorio conviven más de 20 paquetes y aplicaciones, el directorio raíz node_modules llega a ocupar decenas de gigabytes. El parámetro outputFileTracingRoot extrae únicamente las dependencias transitivas utilizadas por la aplicación objetivo en todos los paquetes del workspace, generando un artefacto limpio y listo para producción.
El proceso de rastreo de dependencias y generación del artefacto standalone se ejecuta a través de cuatro etapas deterministas:
Transpilación de TypeScript, ensamblaje de Server Components, Server Actions y emisión de chunks de cliente en .next.
Análisis de archivos JS compilados, detección de importaciones estáticas y dinámicas y resolución de symlinks de pnpm.
Copia física de las dependencias detectadas a .next/standalone/node_modules preservando rutas relativas.
Creación del archivo ligero server.js que configura el servidor HTTP, el enrutamiento y los manejadores de caché.
Para habilitar el modo standalone y garantizar la compatibilidad con monorrepositorios corporativos, configuramos next.config.ts:
import type { NextConfig } from 'next';
import path from 'node:path';
const nextConfig: NextConfig = {
// Activa la generación del paquete aislado en .next/standalone
output: 'standalone',
// Fundamental en monorrepositorios: define el límite raíz del proyecto
// a partir del cual @vercel/nft calcula las rutas de las dependencias
outputFileTracingRoot: path.join(__dirname, '../../'),
// Control granular de inclusión/exclusión de archivos dinámicos que
// el analizador de AST no puede detectar estáticamente
experimental: {
outputFileTracingIncludes: {
// Ejemplo: forzar inclusión de plantillas para generar PDFs o tipografías
'/api/generate-pdf': ['./templates/**/*', './fonts/**/*'],
},
outputFileTracingExcludes: {
// Excluir binarios nativos pesados o paquetes de desarrollo detectados por error
'*': [
'node_modules/@swc/core-linux-x64-gnu',
'node_modules/@swc/core-linux-x64-musl',
'node_modules/@esbuild/**/*',
],
},
},
// Desactivar compresión en Node.js si la ejecuta un proxy inverso (Nginx/Cloudflare)
compress: false,
// Ocultar cabecera x-powered-by para reforzar la seguridad
poweredByHeader: false,
};
export default nextConfig;.next/standalone Generado#Tras ejecutar pnpm build, la jerarquía de salida refleja la posición relativa dentro del workspace:
.next/standalone/
├── apps/
│ └── web/
│ ├── .next/
│ │ ├── BUILD_ID
│ │ ├── prerender-manifest.json
│ │ ├── routes-manifest.json
│ │ └── server/ <-- Payloads de RSC y funciones de servidor
│ ├── package.json
│ └── server.js <-- Punto de entrada principal en Node.js
└── node_modules/ <-- Dependencias mínimas rastreadasNota: Al compilar dentro de un workspace de monorrepositorio (por ejemplo, apps/web), la estructura interna de .next/standalone reproduce la ruta relativa respecto a la raíz del monorrepo. Esto garantiza que los enlaces simbólicos a las dependencias compartidas en el node_modules raíz continúen siendo válidos.
# syntax=docker/dockerfile:1.7
# ── 1. IMAGEN BASE ──
FROM node:22-alpine AS base
ENV NODE_ENV=production
WORKDIR /app
# ── 2. CAPA DE DEPENDENCIAS ──
FROM base AS deps
# libc6-compat es necesario para Turbopack en Alpine Linux
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
# Habilitar corepack e instalar dependencias bloqueadas
RUN corepack enable pnpm && \
pnpm install --frozen-lockfile --prod=false
# ── 3. CAPA DE COMPILACIÓN ──
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. CAPA DE EJECUCIÓN (RUNNER) ──
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV NEXT_TELEMETRY_DISABLED=1
# Crear usuario y grupo de sistema sin privilegios
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# PASO FUNDAMENTAL: Copiar activos estáticos excluidos de .next/standalone
COPY --from=builder /app/public ./public
# Crear directorio .next con los permisos adecuados
RUN mkdir .next && chown nextjs:nodejs .next
# Copiar el paquete autónomo de servidor
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# Copiar los chunks estáticos inmutables en .next/static
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Cambiar a usuario no privilegiado
USER nextjs
EXPOSE 3000
# Ejecutar el servidor standalone directamente con Node.js (sin el CLI next start)
CMD ["node", "server.js"]Un desarrollador activa output: 'standalone', construye la imagen Docker copiando únicamente .next/standalone y ejecuta el contenedor. El sitio abre como HTML crudo sin estilos y la consola acumula errores 404 Not Found en las rutas /_next/static/chunks/....
Causa habitual: Se asume erróneamente que la carpeta standalone contiene todos los archivos del sitio por defecto.
Realidad en producción: El archivo server.js espera encontrar los activos del cliente en ./.next/static. Si no se copian explícitamente desde la etapa de compilación, la hidratación en el navegador falla por completo.
# ❌ INCORRECTO: Solo se copia el directorio standalone
COPY --from=builder /app/.next/standalone ./
# ✅ CORRECTO: Copia explícita y obligatoria de static y public
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/staticEn un monorrepositorio con Turborepo, al compilar la aplicación apps/web en modo standalone, el generador no incluye las dependencias declaradas en la raíz del proyecto o en paquetes compartidos (packages/ui), provocando un error fatal MODULE_NOT_FOUND al arrancar node server.js.
Causa habitual: Se confía en la configuración predeterminada de Next.js, que asume como raíz la carpeta actual apps/web.
Realidad en producción: El algoritmo @vercel/nft restringe la búsqueda a apps/web y no asciende en el árbol de directorios, ignorando el node_modules raíz y los symlinks de pnpm.
// ❌ INCORRECTO: Sin definir la raíz del monorrepositorio
const nextConfig: NextConfig = {
output: 'standalone',
};
// ✅ CORRECTO: Declaración explícita de la raíz del monorrepo
import path from 'node:path';
const nextConfig: NextConfig = {
output: 'standalone',
outputFileTracingRoot: path.join(__dirname, '../../'),
};En un Server Component se efectúa una lectura dinámica en disco: fs.readFileSync(path.join(process.cwd(), 'locales', lang + '.json')). En desarrollo local funciona, pero en el contenedor standalone el proceso se detiene con ENOENT: no such file or directory.
Causa habitual: @vercel/nft opera mediante análisis estático de AST. La concatenación dinámica lang + '.json' impide que el analizador deduzca los nombres exactos de los archivos durante la compilación.
Realidad en producción: Los archivos de traducción se omiten en .next/standalone y no existen físicamente en el contenedor final.
// next.config.ts
// ✅ CORRECTO: Inclusión declarativa de los recursos en el paquete
const nextConfig: NextConfig = {
output: 'standalone',
experimental: {
outputFileTracingIncludes: {
'/api/i18n/**': ['./locales/**/*'],
},
},
};En el archivo Dockerfile se especifica en la directiva CMD la instrucción ["pnpm", "start"] o ["next", "start"].
Causa habitual: Se replican rutinas de despliegue tradicionales sin standalone.
Realidad en producción: next start requiere la presencia del binario CLI de Next.js en node_modules/.bin, el cual ha sido depurado intencionadamente durante la reducción standalone. El contenedor finaliza con el error command not found: next. En modo standalone, el punto de entrada es únicamente node server.js.
# ❌ ANTIPATRÓN: Invocar el CLI de Next.js
CMD ["pnpm", "start"]
# ✅ CORRECTO: Invocación directa del script compilado de servidor
CMD ["node", "server.js"]Configuración de output: 'standalone' y outputFileTracingRoot en next.config.ts.
Ejecución de pnpm build. El motor @vercel/nft analiza las rutas y aísla los archivos mínimos en .next/standalone.
Copia de .next/standalone junto a las carpetas public/ y .next/static/ hacia la imagen final runner.
Asignación de permisos al usuario nextjs:nodejs (UID 1001) y lanzamiento mediante node server.js.