Under the conventional Node.js application deployment model, the container image or server environment packages the entire node_modules directory. For large-scale enterprise Next.js applications, this directory frequently bloats to 800 MB–1.5 GB. It packages not merely runtime transitive dependencies, but also build-time tooling (the TypeScript compiler, Webpack/Turbopack plugins, PostCSS, ESLint, test runners), which are completely extraneous when serving live production HTTP requests.
Deploying this volume of superfluous data introduces severe infrastructure penalties:
To resolve these challenges, the Next.js team engineered the isolated compilation mode: output: 'standalone'.
[Source Project: 1.2 GB]
├── node_modules/ (all dependencies: prod + dev)
├── src/
└── next.config.ts (output: 'standalone')
│
▼ [next build]
┌─────────────────────────┐
│ @vercel/nft AST Analysis│ ──► Import dependency graph tracing
└─────────────────────────┘
│
▼
[.next/standalone: ~65 MB]
├── node_modules/ ──► ONLY actively imported runtime JS files
├── package.json
├── server.js ──► Custom ultra-lightweight Node.js HTTP server
└── .next/server/ ──► Compiled RSCs, Route Handlers, and pages
[Isolated Static Assets] (Deliberately EXCLUDED from standalone)
├── public/ ──► Offloaded directly to CDN / Object Storage
└── .next/static/ ──► Immutable hashed client JS/CSS chunksBeneath the surface of standalone mode lies a dedicated file tracing engine: @vercel/nft (Node File Trace). During the next build phase, the compiler traverses the AST of every server component (RSC), page shell, and Route Handler.
The engine systematically detects all dependency invocation mechanisms:
import { x } from 'pkg').import('pkg')).require('pkg')).path.join(__dirname, 'data.json') and fs.readFileSync(...).Consequently, .next/standalone does not copy packages in their entirety. Instead, it extracts solely the precise .js and .json files from node_modules that are executed at runtime. All devDependencies, unreferenced dead code, tests, and documentation files are cleanly pruned.
The mental model of output: 'standalone' centers on the concept of a Hermetic Runtime Slice combined with an explicit architectural separation between dynamic compute and static content delivery.
Next.js categorizes compilation artifacts into two hermetically isolated domains:
.next/standalone): A self-contained server bundle housing the generated server.js entrypoint and the trimmed dependency slice. It requires no global or local Next.js CLI binaries; it boots directly via node server.js using the core Node.js binary.public/ and .next/static/): Client-side bundles, stylesheets, fonts, and public media files.Incoming Network Ingress
│
▼
┌─────────────────────────────────┐
│ Edge Ingress / Nginx │
└─────────────────────────────────┘
│ │
│ /_next/static/* │ Dynamic routes
│ /favicon.ico, /images/* │ SSR / RSC / Server Actions
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────┐
│ CDN / S3 Bucket / │ │ Next.js Standalone Pod │
│ Nginx Static Cache │ │ (node server.js) │
│ │ │ │
│ • Maximum Cache TTL │ │ • Minimal I/O overhead │
│ • Gzip / Brotli │ │ • Isolated memory pool │
│ • Zero Node.js CPU │ │ • No Next.js CLI runtime │
│ utilization │ │ • Instant graceful restart │
└──────────────────────┘ └──────────────────────────────┘Next.js intentionally omits public/ and .next/static/ from .next/standalone. Serving heavy static assets over the single-threaded Node.js event loop is an enterprise anti-pattern. In high-throughput architectures, static assets must be offloaded directly to Nginx, Traefik, AWS CloudFront, or S3, preserving Node.js execution threads for SSR computations and Server Actions.
However, in self-hosted single-container Docker deployments, these static directories must be explicitly copied into the final runtime layer; otherwise, client-side navigation will encounter 404 errors when requesting manifest and chunk files.
Key concepts in standalone deployment:
In microservice architectures where hundreds of Next.js pods autoscale based on CPU and memory thresholds, shrinking container images from 1.2 GB to 85 MB slashes Image Pull Durations from 45 seconds down to milliseconds. This prevents request dropping during traffic surges.
Corporate security compliance mandates automated vulnerability scanning (Registry Scanning) before container promotion. Standalone builds eliminate compilers, test suites, and build scripts, driving CVE warnings in production containers down to near zero.
In monorepos containing 20+ packages, the root node_modules directory frequently exceeds tens of gigabytes. The outputFileTracingRoot configuration extracts only the transitive dependencies utilized by the target application across all workspace packages, producing a surgical, deployable artifact.
The standalone dependency tracing and artifact generation process proceeds through a deterministic four-stage pipeline:
TypeScript transpilation, Server Components assembly, Server Actions binding, and client chunk emission into .next.
Parsing compiled JS files, identifying static and dynamic import calls, and resolving pnpm symlinks.
Physical copying of identified runtime dependencies into .next/standalone/node_modules preserving relative paths.
Generation of the lightweight server.js script configuring HTTP dispatching, routing, and cache handlers.
To enable standalone mode and ensure compatibility with enterprise monorepos, configure next.config.ts:
import type { NextConfig } from 'next';
import path from 'node:path';
const nextConfig: NextConfig = {
// Activate isolated .next/standalone artifact generation
output: 'standalone',
// Critical for monorepos: defines the project root boundary
// from which @vercel/nft calculates relative dependency paths
outputFileTracingRoot: path.join(__dirname, '../../'),
// Fine-grained inclusion/exclusion for dynamic files that
// the AST analyzer cannot determine statically
experimental: {
outputFileTracingIncludes: {
// Example: force inclusion of PDF generation templates or custom font binaries
'/api/generate-pdf': ['./templates/**/*', './fonts/**/*'],
},
outputFileTracingExcludes: {
// Exclude heavy native binaries or build-only packages captured erroneously
'*': [
'node_modules/@swc/core-linux-x64-gnu',
'node_modules/@swc/core-linux-x64-musl',
'node_modules/@esbuild/**/*',
],
},
},
// Disable Node.js-level compression if handled by edge reverse proxies (Nginx/Cloudflare)
compress: false,
// Mask technology stack by stripping x-powered-by header
poweredByHeader: false,
};
export default nextConfig;.next/standalone Directory#Following a successful pnpm build, the output structure mirrors the relative workspace hierarchy:
.next/standalone/
├── apps/
│ └── web/
│ ├── .next/
│ │ ├── BUILD_ID
│ │ ├── prerender-manifest.json
│ │ ├── routes-manifest.json
│ │ └── server/ <-- RSC payloads and server functions
│ ├── package.json
│ └── server.js <-- Standalone HTTP entrypoint
└── node_modules/ <-- Traced minimal runtime dependenciesNote: When building within a monorepo workspace (e.g. apps/web), the internal folder hierarchy inside .next/standalone mirrors the relative path to the monorepo root. This preserves valid symbolic link resolution to shared dependencies in the root node_modules.
# syntax=docker/dockerfile:1.7
# ── 1. BASE IMAGE ──
FROM node:22-alpine AS base
ENV NODE_ENV=production
WORKDIR /app
# ── 2. DEPENDENCY CACHE LAYER ──
FROM base AS deps
# libc6-compat is required for Turbopack on Alpine Linux
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
# Enable corepack and install exact locked dependencies
RUN corepack enable pnpm && \
pnpm install --frozen-lockfile --prod=false
# ── 3. BUILD LAYER ──
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. PRODUCTION 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
# Establish unprivileged system user and group
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# CRITICAL STEP: Copy static assets excluded from .next/standalone
COPY --from=builder /app/public ./public
# Prepare .next directory with correct ownership
RUN mkdir .next && chown nextjs:nodejs .next
# Copy the hermetic standalone server bundle
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# Copy immutable hashed static chunks to .next/static
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Switch to non-root execution
USER nextjs
EXPOSE 3000
# Execute the standalone server entrypoint directly with Node.js
CMD ["node", "server.js"]A developer configures output: 'standalone', builds the Docker image copying only .next/standalone, and starts the container. The HTML document loads, but renders unstyled, logging cascading 404 Not Found errors for /_next/static/chunks/....
Root Cause: Developers assume standalone contains all client-facing assets by default.
Production Reality: server.js expects client-side assets at ./.next/static. If these are not explicitly copied from the builder stage, client hydration fails completely.
# ❌ INCORRECT: Only copying the standalone directory
COPY --from=builder /app/.next/standalone ./
# ✅ CORRECT: Explicitly copying public and static directories
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/staticIn a Turborepo setup, building the standalone output for apps/web fails to trace dependencies declared in the workspace root or shared packages (packages/ui), triggering fatal MODULE_NOT_FOUND errors when launching node server.js.
Root Cause: Next.js defaults to scoping file tracing strictly to the current working directory (apps/web).
Production Reality: The @vercel/nft tracer truncates file trees at apps/web, ignoring root node_modules and symlinked workspace dependencies.
// ❌ INCORRECT: Omitting monorepo root definition
const nextConfig: NextConfig = {
output: 'standalone',
};
// ✅ CORRECT: Explicitly declaring the monorepo root
import path from 'node:path';
const nextConfig: NextConfig = {
output: 'standalone',
outputFileTracingRoot: path.join(__dirname, '../../'),
};Server Component code dynamically reads disk files: fs.readFileSync(path.join(process.cwd(), 'locales', lang + '.json')). While functional in local development, the containerized standalone build crashes with ENOENT: no such file or directory.
Root Cause: @vercel/nft relies on static AST analysis. Dynamic string concatenation (lang + '.json') prevents the tracer from predicting file paths at build time.
Production Reality: Localization files are omitted from .next/standalone and absent in the runner container.
// next.config.ts
// ✅ CORRECT: Declaratively force-including runtime assets
const nextConfig: NextConfig = {
output: 'standalone',
experimental: {
outputFileTracingIncludes: {
'/api/i18n/**': ['./locales/**/*'],
},
},
};The container CMD instruction is specified as ["pnpm", "start"] or ["next", "start"].
Root Cause: Carrying over legacy deployment habits from non-standalone workflows.
Production Reality: Running next start requires the Next.js CLI binary in node_modules/.bin, which is intentionally purged during standalone pruning. The container crashes with command not found: next. The standalone entrypoint is strictly node server.js.
# ❌ ANTI-PATTERN: Calling the Next.js CLI
CMD ["pnpm", "start"]
# ✅ CORRECT: Direct execution of the generated server entrypoint
CMD ["node", "server.js"]Define output: 'standalone' and configure outputFileTracingRoot in next.config.ts.
Run pnpm build. @vercel/nft analyzes all routes and packages the minimal runtime slice into .next/standalone.
Copy .next/standalone alongside public/ and .next/static/ into the lightweight runner image.
Set ownership to nextjs:nodejs (UID 1001) and launch via node server.js.