2022-06-03

Multi-Stage Builds in Docker

What is Multi-Stage Build

Multi-stage builds are a feature in Docker that help streamline the build process, reduce image size, and enhance security for containerized applications. By allowing multiple FROM statements within a single Dockerfile, multi-stage builds facilitate the creation of temporary intermediate images that only exist during the build process.

Each stage in a multi-stage build can inherit from a different base image, which enables the use of various tools, libraries, and environments tailored to specific tasks. As a result, developers can optimize each stage for its purpose, such as compilation or testing, without affecting the final image. Only the last stage in the build process produces the output image that will be deployed, ensuring that only necessary files and dependencies are included.

Using multi-stage builds not only helps create cleaner and leaner Docker images, but also makes it easier to manage complex build processes. By separating the build process into distinct stages, developers can focus on individual tasks, improve code readability, and simplify troubleshooting when issues arise. Moreover, multi-stage builds offer flexibility in reusing certain stages across different projects, promoting code modularity and reusability.

Benefits of Multi-Stage Builds

Reducing Image Size

One of the primary benefits of multi-stage builds is the ability to minimize the final Docker image size by only including the necessary files and dependencies needed to run the application. Intermediate build stages can contain build tools, compilers, and other dependencies that are required during the build process but not needed in the final image. By copying only the relevant artifacts from the intermediate stages to the final stage, developers can create lean and efficient images that are faster to deploy and consume fewer resources.

Enhancing Security

Multi-stage builds can contribute to enhancing the security of containerized applications by minimizing the number of components in the final Docker image. Reducing the attack surface for potential security vulnerabilities is crucial for maintaining the integrity and safety of applications. By creating focused images with fewer components, developers can more easily manage and maintain security, as well as ensure compliance with organizational policies and industry standards.

Moreover, multi-stage builds allow developers to use separate environments for building and running applications, which can further improve security. For example, a build stage may use an image with various development tools and libraries, while the final stage might use a minimal, production-ready base image that only includes the runtime environment.

Simplifying Build Processes

By breaking down the build process into multiple stages, multi-stage builds simplify complex build processes, making them more readable and maintainable. Each stage can focus on a specific task, such as compiling source code, running tests, or generating assets, making it easier to understand the entire process and troubleshoot issues when they arise.

In addition, multi-stage builds promote modularity and reusability by enabling developers to reuse specific build stages across different projects. For instance, if multiple projects use the same toolchain or build environment, a common intermediate stage can be created and shared among them, reducing code duplication and maintenance effort.

Implementing Multi-Stage Builds

To implement multi-stage builds, follow these steps:

  1. Define the base images for each stage
    Use multiple FROM statements in the Dockerfile to define the base images for each stage. Give each stage a unique alias using the AS keyword, which will be used to reference the stage later.
Dockerfile
# Build stage
FROM node:14 AS build
  1. Specify the working directory
    Set the working directory for each stage using the WORKDIR instruction.
Dockerfile
WORKDIR /app
  1. Copy necessary files and install dependencies
    Copy the required files and install dependencies for each stage using the COPY and RUN instructions.
Dockerfile
# Copy package files and install dependencies
COPY package*.json ./
RUN npm install
  1. Compile or build the application
    Perform any necessary compilation or build steps specific to the application.
Dockerfile
# Copy source files and build the application
COPY . .
RUN npm run build
  1. Define the final stage
    Specify the final stage's base image and set the working directory.
Dockerfile
# Final stage
FROM node:14-alpine AS final
WORKDIR /app
  1. Copy artifacts from previous stages
    Use the COPY instruction with the --from flag to copy artifacts or files from previous stages to the final stage.
Dockerfile
# Copy build artifacts from build stage
COPY --from=build /app/dist /app/dist
  1. Configure the runtime environment
    Set any necessary environment variables, exposed ports, or entry points for the application.
Dockerfile
# Set environment variables
ENV NODE_ENV=production

# Expose the application port
EXPOSE 3000

# Define the entry point
CMD ["npm", "start"]

Building Develop and Production Stages Using the --target Option

In this chapter, I will discuss how to build develop and production stages from a single Dockerfile using the --target option. This allows developers to build Docker images tailored to specific environments without maintaining separate Dockerfiles.

Building the Develop Stage

To build the develop stage from a multi-stage Dockerfile, use the --target option with the docker build command and specify the alias of the develop stage.

For example, given the following Dockerfile:

Dockerfile
# Develop stage
FROM node:14 AS develop
WORKDIR /app
COPY package*.json ./
RUN npm install
ENV NODE_ENV=development
CMD ["npm", "run", "dev"]

# Production stage
FROM node:14-alpine AS production
WORKDIR /app
COPY --from=develop /app/node_modules /app/node_modules
COPY . .
RUN npm run build
ENV NODE_ENV=production
CMD ["npm", "start"]

To build the develop stage, run:

bash
$ docker build -t my-app:develop --target develop .

This will create a Docker image tagged as my-app:develop containing the development environment.

Building the Production Stage

To build the production stage, simply omit the --target option or specify the alias of the production stage.

For example, using the same Dockerfile as above:

To build the production stage, run:

bash
$ docker build -t my-app:production --target production .

This will create a Docker image tagged as my-app:production optimized for deployment in a production environment.

Real-World Examples

In this chapter, I will explore real-world examples of multi-stage builds for different programming languages and frameworks.

Building a Node.js Application

In this example, we will create a multi-stage build for a Node.js application using the node:14 base image for the build stage and the node:14-alpine image for the final stage.

Dockerfile
# Build stage
FROM node:14 AS build
WORKDIR /app

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

# Copy source files and build the application
COPY . .
RUN npm run build

# Final stage
FROM node:14-alpine AS final
WORKDIR /app

# Copy build artifacts from build stage
COPY --from=build /app/dist /app/dist

# Set environment variables
ENV NODE_ENV=production

# Expose the application port
EXPOSE 3000

# Define the entry point
CMD ["npm", "start"]

Compiling a Golang Binary

In this example, we will create a multi-stage build for a Golang application, using the golang:1.17 base image for the build stage and the scratch image for the final stage.

# Build stage
FROM golang:1.17 AS build
WORKDIR /src

# Copy source files and compile the application
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app

# Final stage
FROM scratch AS final
WORKDIR /app

# Copy the compiled binary from the build stage
COPY --from=build /src/app .

# Expose the application port
EXPOSE 8080

# Define the entry point
CMD ["./app"]

Creating a Python Web App

In this example, we will create a multi-stage build for a Python web application using Flask, with the python:3.9 base image for the build stage and the python:3.9-slim image for the final stage.

# Build stage
FROM python:3.9 AS build
WORKDIR /app

# Copy requirements file and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy source files
COPY . .

# Final stage
FROM python:3.9-slim AS final
WORKDIR /app

# Copy installed dependencies and source files from the build stage
COPY --from=build /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY --from=build /app .

# Expose the application port
EXPOSE 5000

# Define the entry point
CMD ["python", "app.py"]

References

https://docs.docker.com/build/building/multi-stage/
https://earthly.dev/blog/docker-multistage/
https://dev.to/pavanbelagatti/what-are-multi-stage-docker-builds-1mi9

Ryusei Kakujo

researchgatelinkedingithub

Focusing on data science for mobility

Bench Press 100kg!