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:
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?
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:
Recommended by LinkedIn
# 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:
RUN adduser -D myuser
USER myuser
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?
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:
Stay connected:
Looking forward to more learning and growth together!