Pushing a Node.js Docker image to ECR was taking 4+ minutes every CI run. docker image ls showed the image was sitting at 1.2GB. Cold pulls in EKS were slow enough to cause pod startup timeouts.
Root cause:
The Dockerfile was using node:18 as the base image and installing all dev dependencies (npm install without --omit=dev). The full Node.js image includes build tools, Python, and compilers that have no place in a production runtime.
Fix:
Before — single-stage Dockerfile (1.2GB):
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]After — multi-stage build (180MB):
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]Check the size difference:
docker build -t my-app:optimized .
docker image ls my-appLesson: Always use multi-stage builds for Node.js, Go, and Java apps — the builder stage carries tools you never need at runtime, and Alpine images alone cut 60–70% of image size.