Docker Image Size Problems? The Hidden Techniques You Can’t Afford to Miss

Docker Image Size Problems? The Hidden Techniques You Can’t Afford to Miss

Are your Docker images piling up in size, slowing down your deployment pipelines, and wasting valuable storage? Optimizing Docker images is more than a best practice; it’s a critical step in ensuring smooth and cost-effective operations. In this guide, we’ll explore practical, field-tested techniques to help you reduce image size, accelerate deployments, and minimize the impact on your storage and bandwidth.


Why Docker Image Size Matters

Before we dive into the “how,” let’s talk about the “why.” Oversized Docker images are more than just hard drive hogs—they bring hidden costs that can add up quickly:

  1. Increased Deployment Time: The bigger the image, the longer it takes to build, transfer, and deploy.
  2. Storage and Bandwidth Costs: Storage, especially cloud-based, isn’t free, and transferring large images can burn through your bandwidth allowance.
  3. System Performance: Heavier images can lead to resource contention and slower runtime, especially when scaled across multiple containers.

Case in Point: Reducing a Node.js Image from 600MB to 30MB

To illustrate the impact of these optimizations, we’ll look at a sample Node.js image. Starting with a basic 600MB image, we’ll go step-by-step, using techniques that shrink it down to an agile 30MB. Here’s the approach:


Step 1: Multi-Stage Builds - Separating Build and Runtime Dependencies

Multi-stage builds are a powerful way to reduce Docker image size by building dependencies only once and copying only the essential output to the final image. Here’s an example Dockerfile:

# Stage 1: Build stage
FROM node:16 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Production stage
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]        

How It Works: By using a multi-stage build, we only include the final dist output and node_modules in our production image, leaving behind all build tools and unnecessary files from the first stage. This change alone can reduce the image by hundreds of megabytes.


Step 2: Choosing Minimal Base Images - Less is More

The base image you choose has a huge impact on your final image size. Instead of using a full node image, consider using node:16-alpine, which is a much smaller version optimized for production.

Why Alpine?

  • Lightweight: Alpine is a slimmed-down Linux distribution that keeps only the essentials.
  • Secure: Fewer packages reduce the surface area for vulnerabilities.

Here’s how it looks:

# Using a minimal base image
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
CMD ["node", "index.js"]        


Note: Alpine is best for applications with fewer system dependencies. If your app needs native modules, additional setup may be necessary.

Step 3: Layer Optimization - Combining Commands to Minimize Layers

Every command in a Dockerfile adds a new layer to the image. The more layers, the larger your image becomes. By combining commands, you can optimize layer usage and keep things cleaner:

# Efficient layer usage
RUN apk add --no-cache curl && \
    apk add --no-cache bash        


This combination not only reduces the number of layers but also keeps temporary files out of the image. Using the --no-cache flag prevents unwanted package manager caches from inflating the image size.


Step 4: Pruning Unnecessary Files with .dockerignore

Docker will copy everything in your project folder unless you tell it not to. This can include logs, local configurations, and build artifacts that bloat your image. Create a .dockerignore file to exclude these files:

# .dockerignore
node_modules
npm-debug.log
*.md
test/        

Adding a .dockerignore file is similar to using a .gitignore—it excludes unnecessary files, which can prevent large directories like node_modules and test folders from being bundled.


Step 5: Slimmer Final Images with Distroless Images

For even leaner images, consider using Distroless, which includes only the necessary dependencies to run an application, removing extra shell commands and package managers. Here’s an example with Node.js:

# Stage 1: Build stage
FROM node:16 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .

# Stage 2: Distroless runtime
FROM gcr.io/distroless/nodejs
COPY --from=builder /app /app
WORKDIR /app
CMD ["index.js"]        

Distroless images are great for security since they lack shell access, and they provide a slim runtime environment—perfect for Node.js microservices.


Step 6: Caching Dependencies

For projects with frequent code changes but static dependencies, caching the node_modules layer can save build time:

COPY package*.json ./
RUN npm install --production
COPY . .        

Since the node_modules layer is created first, Docker will cache it as long as package.json hasn’t changed. This approach significantly speeds up builds when only the application code is modified.


Step 7: Use npm ci Instead of npm install

Switching from npm install to npm ci ensures that your dependencies remain consistent, avoiding unwanted changes and reducing image size. Unlike npm install, npm ci only installs from the package-lock.json file, which helps prevent unnecessary installs and maintains reproducibility.

RUN npm ci --only=production        

Step 8: Analyzing Images with Dive

Using tools like Dive helps analyze each layer of your Docker image, pinpointing where bloating occurs. Dive provides detailed information about each layer’s contents and size, helping you make data-driven decisions on optimizations.


Step 9: Keeping Secrets and Sensitive Data Secure

Sensitive data like API keys or passwords should not be hardcoded in Dockerfiles. Instead, leverage environment variables or Docker secrets. This approach keeps sensitive data secure and prevents it from being embedded directly into images.


Step 10: Essential Security Best Practices

Here are some security best practices to follow when optimizing Docker images:

  • Use Non-Root Users: Running as root can expose vulnerabilities. Create a dedicated user for running the application:

RUN adduser -D myuser
USER myuser        

  • Enable Docker Scan: Regularly scan images to detect vulnerabilities using tools like Trivy.
  • Restrict Network Access: Limit the ports your container exposes to reduce attack surfaces.
  • Limit Resource Usage: Set memory and CPU limits to avoid resource exhaustion.


Results: From 600MB to 30MB

After applying these techniques, our sample Node.js application Docker image shrank from 600MB to 30MB—a 95% reduction. The benefits?

  • Faster Deployments: 70% faster deployment times.
  • Lower Costs: Up to 60% reduction in cloud storage and bandwidth usage.
  • Enhanced Security: Reduced image footprint and minimized attack surface.


Optimizing Docker images for Node.js applications isn’t just about saving space; it’s about delivering applications more efficiently, securely, and cost-effectively. By using multi-stage builds, selecting minimal base images, pruning unnecessary files, and following security best practices, you’ll create leaner, faster, and more reliable Docker images that are ready for production.

With these strategies, you’re equipped to streamline your image build process, improve application performance, and reduce infrastructure costs—turning Docker image optimization into a competitive advantage.


if you enjoyed this article, be sure to:

  • Like and Comment to share your thoughts 💬
  • Follow me for more insights on LinkedIn, Medium, and GitHub

Stay connected:

Looking forward to more learning and growth together!

To view or add a comment, sign in

More articles by Muhammad Rashid

Insights from the community

Others also viewed

Explore topics