Multi stage docker builds

Sumukh JSumukh J
4 min read

Understanding Single-Stage Docker Builds: The Foundation

As you embark on your Docker journey, understanding single-stage builds is an excellent first step. These foundational Dockerfiles are perfect for learning the core concepts of containerization and are often sufficient for smaller, less complex applications.

What is a Single-Stage Docker Build?

At its heart, a single-stage Docker build is straightforward:

  • Base Image: You begin by selecting a base image, which provides the operating system and any pre-installed tools your application needs (e.g., node:18-alpine, python:3.9-slim).

  • Dependency Injection: You then "inject" your application's dependencies. This typically involves copying your source code into the container and installing necessary packages (e.g., npm install, pip install).

  • Run Command: Finally, you define the command that will execute your application when the container starts (e.g., CMD ["node", "app.js"], CMD ["python", "app.py"]).

Example of a Single-Stage Dockerfile (Node.js):

FROM node:18

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

CMD ["npm", "start"]

Why Single-Stage Builds Are Great for Beginners

  • Simplicity: They are easy to read, understand, and write, allowing you to grasp the fundamental Dockerfile commands quickly.

  • Rapid Prototyping: For small projects or initial development phases, single-stage builds get your application containerized quickly.

  • Clear Workflow: The flow from base image to running application is very direct.

The Limitations: Why We Need More

While great for learning, single-stage builds quickly reveal their limitations in a production environment, especially when dealing with typical multi-tier architectures:

  • Large Image Sizes: A single-stage build often bundles build tools, development dependencies, and even source code that isn't needed at runtime. This leads to unnecessarily large images.

  • Security Concerns: Larger images mean a larger attack surface due to the presence of unneeded binaries and libraries.

  • Inefficient Caching: Changes to development dependencies can invalidate cached layers further down the build, leading to slower rebuilds.

Moving Beyond Single-Stage: The Path to Optimization

Consider a common 3-tier architecture:

  • Node.js Backend: Requires Node.js runtime, npm for dependency management.

  • React Frontend: Needs Node.js for building, but then a web server like Nginx for serving static files.

  • Database: (Often external, but sometimes you might containerize a development database.)

As a DevOps professional, you'd typically need to build and package each of these components efficiently. This is where single-stage builds fall short.

Understanding Runtime Environments:

  • Python Backend: Your runtime base image would typically be something like python:3.9-slim-buster or python:3.9-alpine.

  • Java Backend: An openjdk:17-jre-slim or openjdk:17-jdk-slim (if you need the JDK at runtime) would be common.

  • Golang Backend: A major advantage of Go is its static compilation. Your Go executable can often be placed directly into a scratch or alpine image without needing a specific Go runtime.

The Quest for Smaller Images: Distroless and Minimal Base Images

This is where the concept of distroless images becomes critical for production:

  • scratch: The smallest possible image. It contains absolutely nothing. You would use this for statically compiled binaries (like Go) where the executable is self-contained.

  • alpine: A very small Linux distribution, often used as a base for many popular images due to its minimal footprint. It includes a basic package manager (apk).

  • Google's Distroless Images: These images contain only your application and its direct runtime dependencies. They are extremely small and secure as they lack shells, package managers, or any other programs you would expect in a standard Linux distribution. Examples include gcr.io/distroless/nodejs, gcr.io/distroless/python3.

By leveraging these smaller base images and carefully configuring your build environments, you can drastically reduce your final image size, leading to:

  • Faster Image Pulls: Quicker deployments.

  • Reduced Storage Costs: Less space needed in your container registry.

  • Improved Security: A smaller attack surface.

The docker build -t your-image-name . Command:

This command compiles your Dockerfile into a Docker image. The -t flag allows you to tag your image with a name and optional version, making it easy to identify and manage.

Here is an example for a golang based multi stage build dockerfile


FROM golang:1.21 as builder
WORKDIR /app
COPY . .
RUN go build -o main .

FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
0
Subscribe to my newsletter

Read articles from Sumukh J directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sumukh J
Sumukh J