- Ultimate Next.js Standalone Dockerfile Guide (Tiny Images)
Ultimate Next.js Standalone Dockerfile Guide (Tiny Images)
Use output: "standalone" to trim node_modules, fix sharp/image errors, and build minimal Next.js Docker images

🐳 Docker & DevOps Implementation Guides
Complete Docker guides with optimization techniques, deployment strategies, and automation prompts to streamline your containerization workflow.
Related Posts:
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.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
// ... other config
};
export default nextConfig;
The "Perfect" Dockerfile (Reference)
Here is the multi-stage pattern that works for Next.js 16.
# 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 correctly
Why: 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.
COPY --from=builder /app/public ./public COPY --from=builder /app/.next/static ./.next/static
3. Permissions Hell
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.
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


