When I first containerized my Node.js app, I felt pretty good about myself. I had a Dockerfile, I built it, and it worked.
Then I checked the size.
1.2GB. For a single Node.js service.
That’s when reality hit me. My image wasn’t lean—it was obese. It slowed down builds, bloated my CI/CD pipeline, took forever to push to the registry, and ate storage like there was no tomorrow.
So, I put my Docker image on a strict diet. After a few rounds of optimizations, it went from 1.2GB → 250MB → 54MB.
Here’s the story of how I cut the fat—and how you can too.
Step 1: The Heavyweight Start
Here’s what my original Dockerfile looked like:
FROM node:16 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . CMD ["node", "server.js"] Looks innocent, right? But it had several problems:
-
node:16is Debian-based and heavy (~350MB). -
npm installinstalled everything—dev and production dependencies. - No
.dockerignore, so logs, git history, andnode_modulessneaked into the image.
The result? A 1.2GB monster that slowed everything down.
Step 2: Choosing a Leaner Base
The first fix was swapping node:16 for node:16-alpine.
FROM node:16-alpine That one-line change cut my image down to ~250MB.
Lesson: Your base image choice can make or break your build.
⚠️ Caveat: Alpine uses musl instead of glibc. If your app has native modules (sharp, bcrypt, canvas), you may need extra packages.
Step 3: Multi-Stage Builds
My app uses TypeScript, so I had build tools sitting inside the final image. Big mistake. They added hundreds of MBs I didn’t need in production.
Enter multi-stage builds:
# Stage 1: Builder FROM node:16-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Stage 2: Runtime FROM node:16-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY package*.json ./ RUN npm ci --only=production CMD ["node", "dist/server.js"] Now, the final image contains only:
- Compiled JavaScript (
dist/) - Production dependencies
No dev dependencies. No build cache. No clutter.
This dropped my image to ~120MB.
Step 4: Prune and Ignore Junk
Another culprit: files that had no business being in production.
I added a .dockerignore:
node_modules npm-debug.log Dockerfile .dockerignore .git .gitignore *.md tests And I cleaned up caches in the Dockerfile:
RUN npm ci --only=production \ && npm cache clean --force \ && rm -rf /tmp/* End result: no accidental junk, no wasted MBs.
Step 5: Minimize Layers
At first, I had a Dockerfile with multiple RUN statements:
RUN apk add --no-cache python3 RUN npm ci --only=production RUN npm cache clean --force Each RUN adds a layer. I combined them into one:
RUN apk add --no-cache python3 \ && npm ci --only=production \ && npm cache clean --force This small tweak shaved off ~15MB. Not huge, but every MB counts when you’re pulling images in production.
Step 6: Measuring and Iterating
The key to trimming images is measuring:
docker images docker history <image> With docker history, I saw exactly which layer was eating space and optimized from there.
Final Weight Check
- Original: 1.2GB
- After switching to Alpine: ~250MB
- After multi-stage + pruning: 120MB
- After
.dockerignore+ cleanup: 54MB 🎉
That’s a ~95% reduction. Pulls went from minutes to seconds, and CI/CD pipelines stopped crawling.
Lessons Learned
- Pick the right base image – Defaults are rarely optimal.
- Multi-stage builds are gold – Keep dev tools out of production.
- Use
.dockerignorereligiously – Don’t ship junk. - Prune aggressively – Caches, logs, temp files… delete them.
- Measure constantly – Know what’s eating space before fixing it.
Conclusion
Cutting Docker image size isn’t just about bragging rights—it’s about faster deploys, lower registry costs, and fewer headaches.
My Node.js image went on a diet and lost 1.1GB, and I’ll never go back to lazy Dockerfiles again.
If your containers are bloated, trust me: a few tweaks can make them featherweight.
So… is your Docker image on a healthy diet?
📬 Contact
If you’d like to connect, collaborate, or discuss DevOps, feel free to reach out:
- Website: hasan-ashab
- GitHub: github.com/HasanAshab
- LinkedIn: linkedin.com/in/hasan-ashab
Top comments (6)
That's awesome!
Thanks 🙂
Awesome slimming of the Docker image!
I'm glad you like the new image 🤧
Some comments may only be visible to logged-in visitors. Sign in to view all comments.