Docker - The Secret to Small, Secure Images

Multi-stage builds are just one of many best practices. Learn how to build small and secure Docker images.

Kashyap Merai /  · 3 min read

This week, I was waiting (and waiting…) for a 1.2 GB Node.js image to push to a container registry. It was packed with dev dependencies and build tools that had nothing to do in production image.

It was a great reminder of one of the essential patterns in Docker: The multi-stage build

Your 5-Minute Win

The Problem
Most standard Dockerfiles create huge final images because they contain all the build tools, libraries, and dependencies (npm, maven, gcc etc.).

This is slow to transfer, costs more to store, and increases your security attack surface.

The Solution
Use a multi-stage build to separate the build environment from the final runtime environment.

We use a temporary container to build our app, then copy only the finished product into a clean, tiny production image.

Here’s a Dockerfile for a simple Node.js application:

## -- Stage 1: The "Builder" --
# We use a full Node image to build our app
FROM node:18-alpine AS builder
WORKDIR /app

# Copy package files and install dependencies
COPY package*.json ./
RUN npm install

# Copy the rest of our app’s source code
COPY . .

# Build our application (e.g., for a TypeScript or React app)
RUN npm run build

## -- Stage 2: The "Final" Image --
# We start FROM scratch with a new, lightweight image
FROM node:18-alpine
WORKDIR /app

# Copy ONLY the necessary node_modules from the “builder” stage
COPY --from=builder /app/node_modules ./node_modules

# Copy ONLY the built application code from the “builder” stage
COPY --from=builder /app/dist ./dist

# The command to run our optimized app
CMD ["node", "dist/main.js"]

The Breakdown:

  • FROM node:18-alpine AS builder: This AS builder is key. It names our first stage so we can reference it later. This stage has all the tools we need to build our app.
  • FROM node:18-alpine: This line starts a brand new, second stage. Nothing from the builder stage is included by default. It’s a clean slate.
  • COPY —from=builder /app/dist ./dist: This is the magic. It tells Docker: “Copy the /app/dist folder from the builder stage into the ./dist folder of our new, clean final stage.”

The Result: You get a lean, production-ready image that is often 80-90% smaller than the builder image. We went from a bloated 1.2 GB image to a slim 150 MB one, which is faster to deploy and more secure.

The DevOps Radar

Dockerfile Best Practices

https://docs.docker.com/build/building/best-practices

Multi-stage builds are just one of many best practices. This official guide from Docker covers how to write effective, cacheable, and maintainable Dockerfiles. Pay special attention to the section on layer caching.

Hadolint - A Dockerfile Linter

https://devtodevops.com/blog/hadolint-dockerfile/

This tool automatically checks your Dockerfile for common mistakes and best practice violations. Integrating it into your workflow will make you a better Docker user overnight. It’s like a spell-checker for your Dockerfile.

Terminal Tip

Feeling like Docker is eating up all your disk space? It probably is. Dangling images, build caches, and stopped containers pile up. This command is your cleanup crew.

# WARNING: This will remove all stopped containers, all unused networks
# all dangling images, and all build cache

docker system prune -a

Run this command periodically to reclaim gigabytes of disk space.

Over to You

That’s all for this week!

What’s the biggest Docker image you’ve ever had to deal with?

Kashyap Merai

Kashyap Merai

Kashyap Merai, a Certified Solution Architect and Public Cloud Specialist with over 8 years in IT. He helped startups in Real Estate, Media Streaming, and On-Demand industries launch successful public cloud projects.

Passionate about Space, Science, and Computers. He also mentors aspiring cloud engineers, shaping the industry's future.

Connect with him on LinkedIn to stay updated on cloud innovations.