Learn how the docker image can be reduced much more than what you have now.
optimizationdockernextjs
Using docker images for deploying web applications is an standard thing in the industry. But most of the time developers get much higher docker image sizes (1-2 GB) than it actually needs to be.
Why it gets so big?
lets take a look at this quick and easy docker image below
FROM node:20WORKDIR /appCOPY . .RUN npm installRUN npm run buildEXPOSE 3000CMD ["npm", "start"]
$ docker images demo-build REPOSITORY TAG IMAGE ID CREATED SIZEdemo-build latest 372ae8ddb34c 9 minutes ago 2.63GB
When you run docker buiild -t <your_tag> . it copies everything into the docker base image, run npm install then, build and bundle the application. After that, the port 3000 is exposed.
The image can be run with the provided CMD command.
Can you remove any of these steps? No. The correct question is Do you need "Everything" in the docker image to run the application?
If your application has a bundler, the node_modules, source code, other dependency codes are all bundled into the bundler output. So they are not required.
Major forgetting factor
Info
Docker does not automatically "NOT copy" files mentioned in .gitignore
A lot of devs forget to include a .dockerignore file when setting up docker for applications. Most of time this is due to the docker integration is done quite after the project is initialized and they have already setup .gitignore
COPY . .
This command copies everything in the users codebase into the base image working directory. Everything means — node modules, local build artifacts, sensitive information that only should be injected in runtime etc.
So you need something similar to .gitignore, called .dockerignore
It is pretty similar to .gitignore but you can also remove .git folder from docker image. This also helps for size reduction.
Example .dockerignore for a nextjs project
~/.dockerignore
# Ignore node_modules and build filesnode_modules out distnpm-debug.log README.md .next !.next/static !.next/standalone # Ignore logs and temporary files*.log*.lock*.tmp# Ignore git and environment files.git.env.local.env.*
Note
Using .dockerignore has more benefits than just reducing size. for example, if you are developing using a Mac environment, your node_modules might not be compatible with the node:20 base image which runs in a unix environment. .dockerignore fix this with a clean dependency install.
.dockerignore is also essential to stop your .env credentials from leaking into the docker image itself.
Multi Stage Build
when we run npm run build it builds the application for the runtime, but after that, we only need the build artifacts. Node modules, source code information are redundant.
This is where building docker image with multiple steps comes in handy. We can only copy things we need to the next step and ditch all others.
Other ways to reduce image size
1. Choosing the best base image
Instead of choosing a generic unix image like ubuntu which has a lot of generic purpose tools that are not required in your application, you always need to choose the bare minimum docker base image that has everything included to run your application but nothing else.
For projects that require javascript runtime, you can use node:20-slim (Slim edition), node:20-alpine (Alpine edition, which is even smaller), or use Bun Image — oven/bun:1-alpine which can drastically reduce size if your framework supports it.
2. Prune node_modules Junk
Even after running npm ci --omit=dev, installed packages often ship with unused markdown files, tests, source maps, and metadata.
Use node-prune: A CLI utility specifically built to sweep node_modules and delete non-production files (like .md, .ts, .map, and test/ folders).
In Dockerfile:
RUN npx node-prune
(This alone often trims 10%–30% off the node_modules directory).
3. Combine RUN Chains & Minimize Layers
Every RUN, COPY, and ADD directive in a Dockerfile adds an image layer.
so if you create files or install libraries in one RUN command and delete them in a subsequentRUN command, the space is still consumed in the previous layer.
Always chain environment setup and cleanup into a single RUN line using &&:
Thats about what we can achieve with only from docker itself. They rest is up to the framework or yourself.
Framework specific optimizations
Most of frameworks like NextJS, NuxtJS, Astro, SvelteKit offer docker specific build methods to build optimized images. Or more generally, "Container specific build options". These usually provide extra-ordinary optimizations for containerized applications. Most of the size reductions can achieve using these.
for Nextjs, they offer a build type called standalone, use this in next.config.ts
import type { NextConfig } from "next";const nextConfig: NextConfig = { /* config options here */ output: "standalone", /* ... */}; export default nextConfig;
In this build configuration, everything required to run the application will be built into .next/standalone folder with an entry server.js file
Only 3 things required for the application,
Source File / Directory
Target Container Location
Purpose
.next/standalone
/app/
The lightweight server (server.js) and pruned node_modules
.next/static
/app/.next/static
Compiled JavaScript chunks, CSS, and client assets
public
/app/public
Public static assets (images, favicon, fonts)
Key Benefits for Docker Images
Standard Next.js builds with full node_modules typically produce Docker images ranging from 800 MB to 1.5 GB. A standalone build drops the image down to 100 MB – 200 MB.
You don't need npm, yarn, pnpm, or bun installed in your final Docker container—node server.js runs directly on the bare runtime.
Spawning a simple node server.js process is faster than booting up the full next start CLI wrapper.
Dropping dev dependencies, package managers, and raw source code from the final layer removes hundreds of potential CVEs from production environments.
$ docker images optimized-buildREPOSITORY TAG IMAGE ID CREATED SIZEoptimized-build latest 40e56d14644e 5 minutes ago 222MB
Important Caveat: Static Asset Separation
By design, Next.js does not copy static assets into .next/standalone to keep the bundle size isolated. This is due to those can be served via CDNs for maximum optimization.
You must manually copy .next/static and public in your Dockerfile, or serve them via a CDN/Nginx layer in front of your container:
# Critical step in your final Docker stage:COPY --from=builder /app/public ./publicCOPY --from=builder /app/.next/standalone ./COPY --from=builder /app/.next/static ./.next/static
Complete NextJS Optimized Dockerfile
FROM node:20-slim AS baseRUN apt-get update # Install dependencies including dev for buildFROM base AS depsWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci# build appFROM base AS builderWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .ENV NEXT_TELEMETRY_DISABLED=1RUN npm run build# production imageFROM base AS runnerWORKDIR /appENV NODE_ENV=productionENV NEXT_TELEMETRY_DISABLED=1ENV PORT=3010ENV HOSTNAME=0.0.0.0# Set correct permissionsRUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjsCOPY --from=builder /app/public ./public# Make sure cache folder existsRUN mkdir -p .next/cache/images && chown -R nextjs:nodejs .next# copy only what's needed to runCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/staticUSER nextjsEXPOSE 3010# server.js is created by next build from the standalone outputCMD [ "node", "server.js"]