Skip to main content

Command Palette

Search for a command to run...

How I Reduced a Next.js Docker Image from 3.39 GB to 619 MB (82% Reduction)

Updated
9 min read
How I Reduced a Next.js Docker Image from 3.39 GB to 619 MB (82% Reduction)
P
Welcome! I’m Prajwal P. I stand at the intersection of technology and efficiency, exploring the dynamic world of DevOps ⚙️. From mastering Cloud infrastructure to orchestrating containers, I am passionate about automating the complex to create the simple. Join me as I document my learning curve, share technical insights, and navigate the ever-evolving landscape of software deployment.

Introduction

While working on a Next.js application, I noticed something that immediately caught my attention.

The Docker image size was:

3.39 GB

For a frontend application, that felt unusually large.

A large Docker image doesn't just consume storage. It affects almost every stage of the software delivery lifecycle:

  • Slower CI/CD pipelines

  • Longer image push times

  • Longer image pull times

  • Increased registry storage costs

  • Slower deployments

  • Higher bandwidth consumption

  • Longer recovery times during incidents

My goal was simple:

Reduce the image size without affecting application functionality.

Instead of blindly modifying the Dockerfile, I decided to investigate the problem properly and identify exactly where the storage was being consumed.

By the end of the optimization process, I reduced the image size from:

3.39 GB → 619 MB

which resulted in:

≈ 82% reduction

In this blog, I'll walk through:

  • How I investigated the image

  • How I identified the root cause

  • How I validated Standalone Mode compatibility

  • Problems encountered during optimization

  • Docker image analysis techniques

  • The final optimized solution

  • Advantages, disadvantages, and best practices


The Initial Problem

The first thing I checked was the image size.

docker images

Output:

REPOSITORY                                 SIZE
erp-react-ui-development-app               3.39GB

Seeing a frontend image larger than many backend services was a clear indication that something wasn't right.

The question was:

What exactly is consuming all this space?


Step 1: Investigating the Container Filesystem

Instead of guessing, I entered the image and analyzed the filesystem.

docker run -it --rm --entrypoint sh image-name

Then:

du -sh /* 2>/dev/null | sort -hr

Output:

1.4G /opt
1.0G /usr
56M  /root

Immediately, I found the largest directories.

The application was located under:

/opt

which became the next area to investigate.


Step 2: Investigating the Application Directory

Next, I analyzed the application files.

du -sh /opt/app/erp/*

Output:

1013M node_modules
58.5M public
2.5M config

This revealed the first major contributor:

node_modules ≈ 1 GB

But the image was:

3.39 GB

which meant there was still much more space being consumed elsewhere.


Step 3: Analyzing Docker Layers

Docker history is one of the most useful commands when investigating image bloat.

docker history image-name

Output:

RUN npm install                     1.13GB
RUN apk add chromium ...            986MB
RUN npm run build                   271MB
COPY .                              114MB

This was the breakthrough moment.

The image size wasn't caused by the application itself.

It was caused by how the application was being packaged.


Root Cause Analysis

After analyzing the layers, I found four major contributors.

Layer Size
npm install 1.13 GB
Chromium 986 MB
Next.js Build 271 MB
Source Code 114 MB

Problem #1 - Chromium

The Dockerfile installed Chromium:

RUN apk add chromium

This layer alone contributed approximately:

986 MB

Almost 1 GB.

After further investigation, I realized Chromium was no longer required for runtime.

We were shipping nearly 1 GB of software that the application wasn't actively using.


Problem #2 - Build Tools in Production

The image also included:

python3
make
g++
pkgconfig

These packages are useful during compilation.

However, they are not required after the application has been built.

Yet they were still being shipped inside the production image.


Problem #3 - Full Dependency Tree

The image performed:

npm install

inside the final image.

Result:

node_modules ≈ 1 GB

Every dependency was included in production.


Problem #4 - Entire Source Code

The complete source tree was being copied into the runtime image.

This included:

Source files
Configuration files
Build artifacts
Development resources

Most of these are not needed to run the application.


The Discovery That Changed Everything

While discussing optimization options, I learned that Next.js supports:

output: 'standalone'

inside:

next.config.mjs

This feature creates a minimal production runtime containing only the files required to run the application.


Version Compatibility Check

Before enabling Standalone Mode, I wanted to verify whether the project's Next.js version supported the feature.

I checked the version:

npm list next

Output:

next@14.1.3

Standalone output was introduced in:

Next.js 12

and is supported in:

Next.js 12
Next.js 13
Next.js 14
Next.js 15

Since our application was running on:

Next.js 14.1.3

we could safely use:

const nextConfig = {
  output: 'standalone'
}

without requiring a framework upgrade.


How We Verified Standalone Mode Was Compatible

Simply enabling a feature is not enough.

I wanted proof that the application would continue working correctly.


Step 1: Enable Standalone Mode

Inside:

next.config.mjs

I added:

const nextConfig = {
  output: 'standalone'
}

Then rebuilt:

npm run build

Step 2: Verify Standalone Output

Initially, I noticed:

.next/standalone

was not being generated.

After further investigation, I discovered the optimization existed in:

size-optimize

branch and not in:

main

After switching branches and rebuilding:

git checkout size-optimize
npm install
npm run build

I verified:

dir .next\standalone

Output:

.next/standalone
├── server.js
├── node_modules
├── package.json
├── src
└── .env

This confirmed that Next.js had generated the standalone runtime package successfully.


Step 3: Measure Standalone Package Size

I measured the generated runtime files.

(Get-ChildItem .next\standalone -Recurse | Measure-Object Length -Sum).Sum / 1GB

Result:

≈ 116 MB

Next:

(Get-ChildItem .next\static -Recurse | Measure-Object Length -Sum).Sum / 1GB

Result:

≈ 124 MB

Combined runtime assets:

≈ 240 MB

Compared to:

node_modules ≈ 1 GB

the difference was significant.


Step 4: Verify Runtime Startup

I created a lightweight Dockerfile:

FROM node:20.12.2-alpine

WORKDIR /app

COPY .next/standalone ./
COPY .next/static ./.next/static
COPY public ./public

EXPOSE 3037

CMD ["node", "server.js"]

Then built the image:

docker build -t erp-ui:standalone .

and started the container:

docker run -d -p 3037:3037 --name erp-ui-test erp-ui:standalone

Step 5: Functional Validation

I validated:

Container Health

docker ps

Container remained healthy.

Application Access

Opened:

http://localhost:3037

Application loaded successfully.

Runtime Logs

docker logs -f erp-ui-test

No runtime errors.

Memory Usage

docker stats

Container consumed minimal memory while idle.

Only after completing these validations did I conclude that Standalone Mode was fully compatible with the application.


Understanding What Standalone Mode Actually Does

Without Standalone Mode:

Application
    ↓
Full Source Code
    ↓
Full node_modules
    ↓
Build Tools
    ↓
Large Docker Image

With Standalone Mode:

Application
    ↓
Required Runtime Files Only
    ↓
Minimal Dependencies
    ↓
Lightweight Docker Image

Think of it as packaging only what the application actually needs to run.


The Optimized Dockerfile

Instead of installing everything inside the runtime image:

FROM node:20.12.2-alpine

WORKDIR /app

COPY .next/standalone ./
COPY .next/static ./.next/static
COPY public ./public

EXPOSE 3037

CMD ["node", "server.js"]

Now the image only contains:

  • Runtime server

  • Required dependencies

  • Static assets

  • Public assets

Nothing else.


CI/CD Best Practice - Multi-Stage Builds

For production CI/CD pipelines, I recommend:

FROM node:20.12.2-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

RUN npm run build

FROM node:20.12.2-alpine

WORKDIR /app

ENV NODE_ENV=production

COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3037

CMD ["node", "server.js"]

This keeps build dependencies out of the final image.


Results

Image Size

Before:

3.39 GB

After:

619 MB

Reduction:

3.39 GB → 619 MB

Savings:

≈ 2.77 GB
≈ 82% reduction

Build Time Measurements

Next.js Build

npm run build

Result:

10 minutes 5 seconds

Docker Build

docker build --no-cache

Result:

3 minutes 40 seconds

Important:

Standalone mode primarily reduces image size.

It does not significantly reduce Next.js compilation time.


Before vs After

Metric Before After
Image Size 3.39 GB 619 MB
Chromium Present Removed
Build Tools Present Removed
Runtime Dependencies Full Minimal
Registry Storage High Low
Deployment Speed Slower Faster
Image Transfer Heavy Lightweight

Advantages

Smaller Images

3.39 GB → 619 MB

Faster Deployments

Less data to transfer.

Faster Image Pulls

Servers download images more quickly.

Lower Storage Costs

Reduced registry usage.

Better Security

Smaller attack surface.

Cleaner Runtime Environment

Only required dependencies are shipped.


Potential Considerations

Standalone Mode should still be validated for applications that use:

  • Custom Node.js servers

  • Native binaries

  • Hardcoded filesystem paths

  • Runtime-generated assets

Always test thoroughly before production rollout.


Lessons Learned

The biggest lesson from this exercise was:

Never optimize blindly.

The process should always be:

Measure
→ Investigate
→ Validate
→ Optimize
→ Verify

The image size problem wasn't caused by application code.

It was caused by packaging decisions.

By understanding exactly where storage was being consumed, we were able to make targeted optimizations instead of random changes.


Final Thoughts

This wasn't just a Docker optimization exercise.

It was an example of how investigation and measurement often provide more value than immediately jumping into solutions.

A few simple commands helped identify exactly where the storage was being consumed.

Final outcome:

Docker Image Size:
3.39 GB → 619 MB

Reduction:
82%

Storage Saved:
~2.77 GB

Next.js Build Time:
10m 05s

Docker Build Time:
3m 40s

A cleaner, lighter, and production-ready deployment.

If you're deploying Next.js applications in Docker and haven't explored Standalone Mode yet, it's definitely worth investigating.

Happy Optimizing! 🚀

A

Really enjoyed this one. Docker image optimization is one of those topics that most people ignore until build times and image size start hurting real deployments, so it was great to see a practical breakdown instead of just generic advice.

Cutting the image size by that much is impressive, but what I like even more is the mindset behind it — thinking about production efficiency, not just making the app run locally. That’s exactly the kind of detail that makes a big difference when you’re working with CI/CD pipelines and cloud deployments.

I’m also spending more time on DevOps and cloud-related content these days, so posts like this are especially useful because they connect optimization with real-world impact. Solid write-up overall.