Image Size Reduction
Slashed average container image size from 1.2GB to under 470MB, a 61% reduction in storage footprint.
A technical deep dive into optimizing a Node.js microservices architecture by implementing multi-stage Docker builds, resulting in faster CI/CD pipelines, lower bandwidth costs, and a reduced security attack surface.
Illustrative example based on common patterns from similar engagements — client details anonymized.
Slashed average container image size from 1.2GB to under 470MB, a 61% reduction in storage footprint.
Decreased GitHub Actions build and push times by 40%, accelerating the feedback loop for developers.
Eliminated build tools and development dependencies from production images, severely reducing vulnerabilities.
The client, a fast-paced fintech startup, had embraced microservices and Docker early in their journey. Their backend consisted of approximately 15 distinct Node.js (TypeScript) services deployed on Amazon EKS (Elastic Kubernetes Service).
While Docker provided the necessary isolation and consistency between environments, they were running into significant operational friction. The primary issue was the size of their Docker images. A typical microservice image was weighing in at over 1.2GB.
These bloated images were causing cascading problems across their infrastructure. CI/CD pipelines in GitHub Actions were taking over 15 minutes to build, tag, and push images to the Elastic Container Registry (ECR). Deployments to EKS were sluggish because nodes took a long time to pull the massive images. Furthermore, their ECR storage costs and cross-AZ data transfer out costs were steadily climbing.
An audit of their existing Dockerfile configurations quickly revealed the root causes of the bloat. The standard Dockerfile they were using for all services looked something like this:
FROM node:18 WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . RUN npm run build EXPOSE 3000 CMD ["npm", "run", "start"]
This seemingly innocent file contained several fatal flaws for production use:
node:18 is based on Debian Bullseye, which includes hundreds of megabytes of OS utilities, compilers, and libraries completely unnecessary for running a compiled Node app.npm install without flags installed massive development dependencies (like TypeScript, Jest, ESLint) directly into the final image.build step.To solve these issues, I introduced multi-stage Docker builds combined with a highly optimized Alpine Linux base image. Multi-stage builds allow you to use multiple FROM statements in a single Dockerfile. Each FROM begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don't need in the final image.
Here is the revised architecture of the Dockerfile:
# Stage 1: Build Environment FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ # Install ALL dependencies (including devDependencies) RUN npm ci COPY . . # Compile TypeScript to JavaScript RUN npm run build # Stage 2: Production Environment FROM node:18-alpine AS runner WORKDIR /app # Set environment to production ENV NODE_ENV=production COPY package*.json ./ # Install ONLY production dependencies RUN npm ci --only=production # Copy ONLY the compiled javascript from the builder stage COPY --from=builder /app/dist ./dist EXPOSE 3000 CMD ["node", "dist/main.js"]
Key Improvements:
node:18 to node:18-alpine instantly shaved over 700MB off the underlying OS layer.builder stage contains all the heavy compilation tools and source code. The runner stage is kept pristine, receiving only the final compiled assets.npm ci ensures deterministic builds based on the lockfile, and --only=production prevents hundreds of megabytes of dev-dependencies from entering the final layer.The rollout of the new multi-stage builds across the 15 microservices yielded immediate and measurable benefits:
1. Massive Storage Reduction (61%): The average image size dropped from 1.2GB to 470MB. This drastically reduced ECR storage costs and lowered the cross-AZ data transfer fees incurred when EKS nodes pull images.
2. Accelerated Deployments: Because the nodes had to download 61% less data, pod startup times in EKS improved significantly. The total time from a developer merging a PR to the code running live in production dropped by 40%.
3. Enhanced Security Posture: By removing development tools, compilers (like `tsc`), and extraneous OS packages (via Alpine), we drastically reduced the attack surface of the production containers. Container security scanning tools (like Trivy) reported a 75% reduction in flagged CVEs simply because the vulnerable packages were no longer present in the final image.
Optimizing Dockerfiles is often overlooked as a premature optimization, but in a microservices environment, container bloat scales linearly. Implementing multi-stage builds fundamentally improved the performance and economics of their entire delivery pipeline.