A real journey of "less is more" in Docker π³
π Table of Contents
- π― The Problem: Bloated Docker Image
- π The Investigation
- π§ The Solution Explained
- π Before vs After: What Changed?
- π Conclusion
- π Reference
π― The Problem: Bloated Docker Image
A few weeks ago, I built a Docker image for my personal portfolio project (a React app). Everything worked fine β until I checked the size...
π£ 1.5 GB! For a static frontend app?!
I realized this could slow down deployment, consume more storage, and make CI/CD pipelines sluggish. I had to fix this.
π The Investigation
When I looked into my Docker setup, I noticed some rookie mistakes:
- I didnβt have a
.dockerignore
file π - I was using only a single-stage build
- The base image was
node:18-alpine
, which seemed fine but not optimal for production delivery - I was copying everything (including
node_modules
,.git
, and build files) into the image
This is my previous Dockerfile:
# Use official Node image
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy dependency files first (for better Docker caching)
COPY package.json package-lock.json ./
# Install dependencies
RUN npm ci
# Copy rest of the application
COPY . .
# Build the React app
RUN npm run build
# Install a lightweight static server
RUN npm install -g serve
# Expose port
EXPOSE 3000
# Serve the static build
CMD ["serve", "-s", "build", "-l", "3000"]
It was time to clean house.
π§ The Solution Explained
1. Add dockerignore
Think of .dockerignore
as .gitignore
for Docker. Without it, Docker copies everything from your working directory β including huge folders like:
node_modules/
.git/
build/
-
*.md
docs
I added this .dockerignore
:
node_modules/
npm-debug.log
.git
.gitignore
Dockerfile
docker-compose.yml
build/
dist/
.env
*.md
π Result: Way less junk copied into the image = faster build time + smaller size
2. Use Multi stage Builds
Instead of one bloated Dockerfile, I split it into two stages:
- Builder Stage: Installs dependencies and runs the build
- Production Stage: Copies only whatβs needed to serve the app
# Stage 1: Build
FROM node:18-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Serve
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/build ./build
RUN npm install -g serve
EXPOSE 3000
CMD ["serve", "-s", "build", "-l", "3000"]
π Result: No dev dependencies or source code in the final image. Just the build output. Lean & clean.
3. Choose Lighter Base Images
Originally I used node:18-alpine
. Itβs small but sometimes causes compatibility issues with native modules.
I switched to node:18-slim
, which is slightly larger than Alpine but more stable for multi-stage builds and still much smaller than the full Node image.
π Result: Better balance between size and reliability.
π Before vs After: What Changed?
Aspect | Before | After |
---|---|---|
π¦ Image Size | ~1.5 GB | ~200 MB |
βοΈ Build Strategy | Single-stage | Multi-stage |
π Base Image | node:18-alpine |
node:18-slim |
π« .dockerignore | β None | β Included to skip unneeded files |
π Copied Files | Everything (including node_modules ) |
Only build/ output copied |
π Performance | Slow build + large deploy | Faster build + minimal production image |
π Conclusion
If you apply the same strategies, youβll get:
- β Smaller Docker Images β Faster pulls/pushes, lower storage cost
- β‘ Faster Builds β Especially in CI/CD pipelines
- π Better Security β No unnecessary source code or secrets inside image
- π Portable & Lightweight Images β Ideal for microservices, serverless, and edge deployments
π Reference
All the code is live here on my GitHub:
π mrzaizai2k/Portfolio
Feel free to clone it, star it β, or ask me anything.
Thanks for reading!
If this helped you, share it with a friend who might still be shipping 1GB containers π
Top comments (1)
βFrom 1.5GB to 200MB? Thatβs massive. Just tried this on my project β game changer πβ