DEV Community

SHUBHENDU SHUBHAM
SHUBHENDU SHUBHAM

Posted on

The Docker Diet: How I Lost 1.1GB in 5 Steps

Last week, I was staring at a 1.2GB Docker image wondering where I went wrong. The build took forever, deployments were slow, and my registry storage costs were through the roof. Sound familiar?
After some serious container weight loss surgery, I managed to get that same image down to 98MB. Here's exactly how I did it.

Let's break down "Docker Diet" into 5 simpler steps:-

Phase 1: The Alpine Cleanse
Phase 2: Multi-stage Meal Prep
Phase 3: Cache Detox
Phase 4: Layer Liposuction
Phase 5: Final Weigh-IN

The Starting Point: My Chunky Container

My original Dockerfile looked innocent enough:

FROM node:22
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

Simple, right? But this little guy was packing 1.2GB of unnecessary baggage. Time for an intervention.

Phase 1: The Alpine Cleanse

First step was ditching the bloated base image. Ubuntu-based Node images are huge. Alpine Linux? Tiny and efficient.

FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

Result: Down to 680MB. Not bad, but we're just getting started.
The difference is massive - Alpine gives you a full Linux distro in about 5MB instead of 100MB+. Your app doesn't care what's underneath as long as Node runs.

Phase 2: Multi-Stage Meal Prep

This is where the magic happens. Think of it like meal prepping - you do all the messy cooking in one kitchen, then serve the clean final product.

# Build stage - the messy kitchen
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Runtime stage - the clean serving plate
FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY src/ ./src/
COPY package*.json ./
EXPOSE 3000
CMD ["node", "src/server.js"]
Enter fullscreen mode Exit fullscreen mode

Result: Down to 320MB. We're getting somewhere!
The builder stage contains all the npm install mess, but the final image only gets the clean node_modules folder. All the npm cache and intermediate files? Gone.

Phase 3: Cache Detox

Time to clean house. Package managers love to hoard cache files, and we need to evict them in the same layer they're created.

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY src/ ./src/
COPY package*.json ./
EXPOSE 3000
CMD ["node", "src/server.js"]
Enter fullscreen mode Exit fullscreen mode

Result: Down to 180MB. The cache cleanup made a bigger difference than expected.
Docker creates a new layer for each RUN command. If you install packages in one RUN and clean cache in another, the cache still exists in that earlier layer. Combining them into one RUN command actually removes the cache from the final image

Phase 4: Layer Liposuction

Now we get surgical. Every COPY and RUN creates a layer. Time to be strategic about what goes where.

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force

FROM node:22-alpine AS runtime
RUN addgroup -g 1001 -S nodejs && adduser -S nodeapp -u 1001
WORKDIR /app
COPY --from=builder --chown=nodeapp:nodejs /app/node_modules ./node_modules
COPY --chown=nodeapp:nodejs src/ ./src/
COPY --chown=nodeapp:nodejs package*.json ./
USER nodeapp
EXPOSE 3000
CMD ["node", "src/server.js"]
Enter fullscreen mode Exit fullscreen mode

Result: Down to 120MB. Plus bonus security points for non-root user.
The --chown flag sets ownership without needing extra RUN commands. Less layers = smaller image. And running as non-root? That's just good manners.

Phase 5: The Final Weigh-In

Last step - a proper .dockerignore diet. This file is like telling Docker "don't even look at this stuff."

Create .dockerignore:

node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.nyc_cache
docs/
tests/
*.test.js
Enter fullscreen mode Exit fullscreen mode

Final result: 98MB. Mission accomplished.

Now coming to the main question

Why This Matters?

Beyond the obvious storage savings, smaller images mean:

  • Faster builds and deployments
  • Lower registry costs
  • Reduced attack surface
  • Happier developers (nobody likes waiting for slow pulls)

The multi-stage approach is the real game-changer here. It lets you have your cake and eat it too - use all the build tools you need, then ship only what actually runs your app.

One More Thing

If you're working with compiled languages like Go, the size reduction can be even more dramatic:

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o main .

FROM scratch
COPY --from=builder /app/main /main
EXPOSE 8080
CMD ["/main"]
Enter fullscreen mode Exit fullscreen mode

That Go app? It'll probably be under 20MB.

The Docker diet isn't about starving your containers - it's about feeding them only what they actually need. And trust me, they'll thank you for it.

Top comments (0)