I kept seeing Dockerfiles for Next.js 16 that were either 2GB heavy or missing critical production optimizations. After debugging my third "why is image optimization failing in production" error, I finally sat down to understand exactly what output: "standalone" does under the hood. Here's what I learned.
What It Is
A production-grade Next.js Dockerfile isn't just about getting the app to run. It's about stripping away the massive node_modules directory that you don't need and keeping only the tiny subset of files you do.
In Next.js 16, this is powered by Output Standalone. When you enable this, Next.js traces every import in your application and copies only the necessary files from node_modules into a .next/standalone directory.
Instead of shipping your entire development dependency tree to production, you ship a calculated, minimal server.
Mental Model: The Surgeon vs. The Mover
Think of a standard Docker build like hiring a moving company. They pack everything in your house—every dusty box, every broken chair—and ship it to the new place. It works, but it's expensive and slow.
Think of output: "standalone" like a surgeon. It operates with precision, cutting out exactly the tissue (code) needed to keep the patient (app) alive and leaving everything else behind.
Your node_modules might differ by 500MB between these two approaches.
Configure It First
Before touching Docker, you must enable this in your specific Next.js config. It does not happen by default.
Here is the multi-stage pattern that works for Next.js 16.
dockerfile
# 1. Base image (use Alpine for size)
FROM node:20-alpine AS base
# 2. Dependencies - install only what's needed for install
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copy package managers
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN npm ci
# 3. Builder - rebuild the source code
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Run the build (creating .next/standalone)
RUN npm run build
# 4. Runner - the final production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Don't run as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# KEY STEP: Install Sharp in the runner
# Standalone build does NOT bundle sharp automatically
RUN npm install sharp
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
When To Use It
Use this pattern when you are Self-Hosting.
Deploying to a VPS (Hetzner, DigitalOcean)
Deploying to a container platform (Fly.io, Cloud Run, Railway)
Deploying to Kubernetes
If you are paying for compute by the GB of RAM or storage, this optimization pays for itself immediately.
When NOT To Use It
Do not use this if you are deploying to Vercel. Vercel's build pipeline handles strict output tracing and optimization automatically. You adding a Dockerfile there is redundant.
Also, avoid standalone if you are using a custom server (e.g., server.js with Express). The tracing logic in Next.js does not automatically trace dependencies required by your custom server entry point.
Gotchas & Common Mistakes
1. The "Sharp is Missing" Error
You'll likely see this error in your logs:
Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctlyWhy: The standalone trace includes your code, but sharp is a native module that often gets excluded or has architecture mismatches (e.g., built on Mac M1, running on Linux Alpine).
Fix: You must explicitly npm install sharp inside the runner stage, or ensure the architecture matches exactly. The specific RUN npm install sharp line in the Dockerfile above handles this.
2. Missing CSS or Images
If your app loads but styles are broken or images 404, you forgot to copy the assets.
Why: The standalone folder contains logic, but it does not contain your public folder or the compiled static assets (.next/static).
Fix: You must manually COPY both of these folders from the builder stage to the runner stage.
Running as root inside Docker is a security risk. But running as nextjs often leads to EACCES errors when Next.js tries to write to the ISR cache.
Why: The .next directory needs to be writable by the user running the process.
Fix: Create the user/group explicitly and chown the directories.
dockerfile
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN chown nextjs:nodejs .next
Conclusion
The output: "standalone" mode in Next.js 16 is powerful, but it requires you to respect its boundaries. It gives you a surgically precise production artifact, but it relies on you to stitch the patient back together (copying assets, installing native modules) correctly.
If you control your own infrastructure, this is the only way to fly.
Thanks, Matija