"Why is our deployment taking 15 minutes?" my manager asked. The answer was embarrassingly simple: we were pushing a 2GB Docker image. Here's how I fixed it.
📦 The 2GB Problem
Our original Dockerfile was... let's call it "naive":
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]
Simple, right? But that node:18 base image is already ~900MB. Add node_modules, build artifacts, and suddenly you're pushing 2GB to your registry every deployment.
🏗️ Multi-Stage Builds
The first optimization: separate build and runtime. Why ship your build tools to production?
# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
Result: 1.2GB → 450MB. Better, but we can do more.
🏔️ The Alpine Switch
Alpine Linux images are tiny. The trade-off? They use musl instead of glibc, which can cause issues with some native modules. For our Node.js app, it worked perfectly:
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]
🎉 The Final Result
Before: 2.1GB
After: 47MB
Reduction: 97.8%
Deployment time dropped from 15 minutes to under 2 minutes. Registry costs went down. Developers stopped complaining about slow builds. Everyone wins.
Use Multi-Stage
Don't ship build tools to production
Choose Alpine
When possible, use -alpine variants
Production Only
Use --only=production for npm
Non-Root User
Security bonus: run as non-root
What's your Docker image diet story? Share your before/after!