All posts

The Diet: Shrinking Your Docker Images with Multi-Stage Builds

In our previous posts, we fixed the plumbing and secured the memory. Now, it is time to look in the mirror. Is your Docker image too big?

When you first start building images, it is common to end up with files that are 900MB or larger. These heavy images take longer to upload to your registry, longer to pull onto your Proxmox server, and they often contain security vulnerabilities you do not need.

Today, we are putting our images on a diet using Multi-Stage Builds.

The Problem: The Single-Stage Bloat

Imagine you are building a React or Node.js application. To build the app, you need tools like npm, compilers, and source files. However, once the app is "built" into a production folder, you do not need npm or the source code anymore. You only need the final files and a tiny web server.

In a traditional single-stage Dockerfile, all those build tools stay inside the final image. This is like keeping the construction crane inside the house after you have finished building it.

The Solution: Multi-Stage Builds

Multi-stage builds allow you to use multiple FROM statements in one Dockerfile. You use one "stage" to build your app and a second "stage" to actually run it.

Here is how the logic works:

  1. Stage 1 (The Builder): You use a full image with all the tools needed to compile your code.
  2. Stage 2 (The Production Image): You start with a tiny, slim image (like Alpine Linux). You copy only the finished files from the first stage and leave everything else behind.

A Practical Example (Node.js)

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

# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
# We only copy the 'dist' folder from the builder stage
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/app.js"]

Why This Matters

  • Smaller Size: An image can drop from 900MB to 50MB just by switching to a multi-stage build with an Alpine base.
  • Better Security: Since the final image does not have compilers or package managers, there is a much smaller attack surface for hackers.
  • Faster Deployments: In my home lab, pulling a 50MB image is nearly instant compared to waiting for a massive 1GB file.

Best Practices for a Lean Image

  • Use .dockerignore: Just like .gitignore, this tells Docker to ignore files like node_modules or local logs during the build.
  • Combine RUN Commands: Every RUN command creates a layer in your image. Combining them using && helps keep the layer count low.
  • Pick Official Images: Always try to use official images from Docker Hub to ensure they are updated and secure.

Wrapping Up

A lean image is a fast image. By using multi-stage builds, you ensure that your production environment only contains exactly what it needs to run.

In the next post, we are going to look at The Conductor. We will move beyond single containers and learn how to use Docker Compose to run entire stacks with a single command.

Happy Dockerizing!