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

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! 🚀



