DEV Community

Cover image for Multi-Stage Dockerfiles
Kostas Kalafatis
Kostas Kalafatis

Posted on

Multi-Stage Dockerfiles

In the previous posts, we explored Docker registries, covering both private and public options. We set up our private Docker registry to manage and store our Docker images securely. Additionally, we covered configuring access and storing Docker images on Docker Hub. Now, in this next chapter, we'll delve into the concept of multi-stage Dockerfiles. This technique allows us to streamline Docker image builds by using multiple stages to optimize build efficiency and reduce image size.

Multi-stage Dockerfiles, introduced in Docker version 17.05, are designed to enhance Docker image optimization for production environments by minimizing image size. This approach involves creating multiple intermediate Docker images during the build process. Each stage focuses on specific tasks and selectively copies essential artifacts to subsequent stages, thereby streamlining the final image composition.

Before the advent of multi-stage builds, the builder pattern was employed to achieve similar optimization goals. However, this method required two Dockerfiles and a shell script, adding complexity to the build process.

We'll begin this post by examining traditional Docker builds and the challenges they pose. Then, we'll explore the builder pattern's implementation for optimizing image size, highlighting its limitations and complexities. Finally, we'll delve into multi-stage Dockerfiles, demonstrating how they effectively address these issues, offering a more streamlined approach to Docker image optimization.

Normal Docker Builds

We can use Dockerfiles to create our own Docker images. As we discussed in the previous posts, a Dockerfile is a text file that instructs docker on how to make an image. But when we run these Docker images in production environments. it is important to have images that are as small as possible.

Consider an example where we build a simple Java application. We are going to deploy a HelloWorld application written in Java using the following Dockerfile:

# Use the official openjdk image as a base image
FROM openjdk:latest

# Set the working directory in the Docker container
WORKDIR /app

# Copy the HelloWorld.java file into the Docker container
COPY HelloWorld.java .

# Compile the HelloWorld.java file
RUN javac HelloWorld.java

# Specify the command to run when the Docker container starts
CMD ["java", "HelloWorld"]
Enter fullscreen mode Exit fullscreen mode

This Dockerfile is designed to build and run a Java application inside a Docker container. It starts by specifying the base image as openjdk:latest, which provides an environment with OpenJDK 11 installed. The WORKDIR /app instruction sets the working directory inside the container to /app, ensuring all subsequent commands are executed relative to this directory.

Next, the COPY HelloWorld.java command copies the HelloWorld.java file from the host machine into the /app directory of the Docker container. This file contains the source code for the Java program.

Following that, the RUN javac HelloWorld.java command compiles the HelloWorld.java file within the Docker container using the Java compiler (javac). This step generates the corresponding bytecode (HelloWorld.class) necessary to run the Java application.

Lastly, the CMD ["java", "HelloWorld"] instruction specifies the command that should be executed when the Docker container starts. Here, it runs the Java Virtual Machine (java) with HelloWorld as the main class. This runs the compiled Java program, printing "Hello, World!" or any other specified output defined within HelloWorld.java.

The following is the content of the HelloWorld.java file. This is a simple file that will print the text "Hello World!" when executed:

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Once the Dockerfile is ready, we can build the Docker image using the docker image build command. This image will be tagged as helloworld:v1

docker image build -t helloworld:v1 .
Enter fullscreen mode Exit fullscreen mode

Now, observe the built image with the docker image ls. You will get an output similar to the following

REPOSITORY                                      TAG       IMAGE ID       CREATED        SIZE
helloworld                                      v1        3dbb42c8d45c   2 days ago     470MB
Enter fullscreen mode Exit fullscreen mode

Take a look at the image size—a whopping 470MB! That's a huge Docker image for a production environment. Not only will it hog bandwidth and time when you're pushing and pulling it across networks, but it's also a potential security risk. Why? Because it likely contains a bunch of build tools that could be vulnerable to attacks.

To make your image leaner and meaner, the best practice is to strip it down to the bare essentials: the compiled code and the runtime environment it needs to function. Think of it this way: you need a hammer to build a house, but once it's built, you don't need to carry the hammer around everywhere you go.

Take Java, for instance. The Java compiler is essential for building the application, but it's unnecessary baggage once the app is up and running. Ideally, you want a minimal Docker image that only contains the Java runtime and nothing else. This reduces the attack surface, making it harder for malicious actors to exploit vulnerabilities.

The Builder Pattern

The builder pattern (which is different from the builder design pattern) is a clever technique for creating Docker images that are as small as possible. It involves two Docker images working together to selectively copy only the essential parts of your application from one image to the other.

The first image is called the build image. It's like your workshop, where you have all the tools you need to build your application from the source code. This includes things like compilers, build tools, and any libraries or dependencies your application needs during the build process.

The second image, aptly named the runtime image, is where the magic happens. It's the environment where your compiled application actually runs. This image is a minimalist, containing only the essential components: your executable files, any necessary runtime dependencies, and the runtime tools needed to execute your code.

To get these essentials from the build image to the runtime image, a shell script comes into play. It uses the docker container cp command to selectively copy only the required files, leaving behind the bulky build tools and unnecessary dependencies.

The entire process of building the image using the builder pattern consists of the following steps:

  1. Create the Build Docker image.
  2. Create a container from the Build Docker image.
  3. Copy the artifacts from the Build Docker image to the local filesystem.
  4. Build the Runtime Docker image using copied artifacts.

Image description

The diagram clearly demonstrates the builder pattern workflow:

  1. Build Container: The Build Dockerfile is responsible for creating the build container. This container comes packed with all the tools needed to compile your source code, like compilers, build tools, and development dependencies. Think it as a well-equipped workshop.
  2. Shell Script Action: Once the Build Container is up and running, a handy shell script takes charge. It uses the docker container cp command to carefully transfer only the necessary executable files from the build container to the Docker host (a.k.a., your local machine).
  3. Runtime Container: Finally, the Runtime Dockerfile comes into play. This Dockerfile is specifically designed to create a lean, mean runtime container. It takes the executable files copied over from the build container and packages them together with any essential runtime dependencies. This lightweight runtime container is optimized for deployment end efficient execution of your application.

This two-step process allows us to keep the final Docker image small and secure by leaving behind the bulky build tools and unnecessary dependencies.

Building a Docker Image with the Build Pattern

In this example, we are going to optimize our Docker image using the builder pattern

Create a new directory named builder-pattern for this example

mkdir builder-pattern
Enter fullscreen mode Exit fullscreen mode

Navigate to the newly created builder-pattern directory

cd builder-pattern
Enter fullscreen mode Exit fullscreen mode

Within the builder-pattern directory, create a file named Welcome.java. This file will be copied to the Docker image at build time

code Welcome.java
Enter fullscreen mode Exit fullscreen mode

Add the following content to the Welcome.java file, and then save and exit this file

public class Welcome { 
    public static void main(String[] args) {
        System.out.println("Hello From Docker!");
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a simple Hello World application written in Java. This will output "Hello From Docker!" once executed.


Within the builder-pattern directory create a file named Dockerfile.build. This file will contain all the instructions that you are going to use to create the build Docker image

code Dockerfile.build
Enter fullscreen mode Exit fullscreen mode

Add the following content to the Dockerfile.build file and save the file

FROM openjdk:latest

# Set working directory inside the builder image
WORKDIR /app

# Copy the Java source file into the builder image
COPY HelloWorld.java /app

# Compile the Java source file
RUN javac HelloWorld.java

# Create the JAR file
RUN jar cvf HelloWorld.jar HelloWorld.class
Enter fullscreen mode Exit fullscreen mode

It starts by selecting the openjdk:latest image from Docker Hub, which provides the latest version of OpenJDK, an open-source implementation of the Java Platform, Standard Edition. This choice ensures that the Docker environment includes all necessary tools and libraries to compile and run Java applications.

Next, the working directory inside the Docker container is set to /app using the WORKDIR /app command. This directory will serve as the context for all subsequent commands in the Dockerfile, simplifying file operations and ensuring that files are copied and created in the correct location.

The COPY HelloWorld.java /app command copies the HelloWorld.java source file from the host machine into the /app directory of the Docker container. This file contains the Java source code for the application that we want to compile and package.

To compile the Java source code into bytecode, the Dockerfile uses the RUN javac HelloWorld.java command. This command executes the Java compiler (javac) inside the Docker container, transforming the human-readable Java source code (HelloWorld.java) into machine-readable bytecode (HelloWorld.class). The resulting .class file contains the compiled Java application ready for execution.

Finally, the Dockerfile creates a JAR file (HelloWorld.jar) from the compiled HelloWorld.class file using the RUN jar cvf HelloWorld.jar HelloWorld.class command. Here, the jar command is invoked with options cvf, where:

  • c: create a new JAR file
  • v: generate verbose output (optional but helpful for debugging)
  • f: specify the filename for the JAR file (HelloWorld.jar) The command packages the compiled Java application into a standalone JAR file, encapsulating all necessary resources and dependencies for the application.

Next, create the Dockerfile for the runtime container. Within the builder-pattern directory, create a file named Dockerfile. This file will contain all the instructions that we are going to use to create the runtime Docker image

code Dockerfile
Enter fullscreen mode Exit fullscreen mode

Add the following content to the Dockerfile and save it

FROM openjdk:11-jdk-slim

# Set working directory inside the builder image
WORKDIR /app

# Copy the Java source file
COPY HelloWorld.jar /app/HelloWorld.jar

CMD ["java", "-jar", "HelloWorld.jar"]
Enter fullscreen mode Exit fullscreen mode

This Dockerfile is designed to run a Java application packaged as a JAR file within a Docker container. It begins by selecting the openjdk:11-jdk-slim image from Docker Hub. This image provides a minimalistic Java Development Kit (JDK) environment based on OpenJDK 11, optimized for smaller container size and efficient resource usage.

The working directory inside the Docker container is set to /app using the WORKDIR /app command. This directory will serve as the context for all subsequent commands in the Dockerfile, ensuring that files are copied and executed from the correct location.

The COPY HelloWorld.jar /app/HelloWorld.jar command copies the HelloWorld.jar file from the host machine into the /app directory of the Docker container. This JAR file is assumed to contain the compiled Java application (HelloWorld.class and its dependencies) that we want to run.

The Dockerfile concludes with the CMD ["java", "-jar", "HelloWorld.jar"] command. This command specifies the default command to run when the Docker container starts. Here:

  • java is the command to execute the Java Virtual Machine (JVM).
  • -jar indicates that the next argument (HelloWorld.jar) is the path to the JAR file that contains the Java application to be executed.

When the Docker container is launched, it will execute the specified java -jar HelloWorld.jar command, which starts the JVM and runs the Java application encapsulated within the HelloWorld.jar file.


Now, let's create a shell script to copy the executables between Docker containers. Within the builder-pattern directory, create a file named build.sh. This file will contain the steps to coordinate the build process between the two Docker containers

code build.sh
Enter fullscreen mode Exit fullscreen mode

Add the following content to the shell script and save the file

set -x

IMAGE_NAME="welcomeapp"
BUILDER_IMAGE_NAME=$IMAGE_NAME"-builder"
IMAGE_TAG="latest"

echo "Building $BUILDER_IMAGE_NAME:$IMAGE_TAG"

# Clean up the existing build
rm -rf target

# Build the builder image
docker build -t $BUILDER_IMAGE_NAME:$IMAGE_TAG -f Dockerfile.build .
if [ $? -ne 0 ]; then
    echo "Failed to build $BUILDER_IMAGE_NAME:$IMAGE_TAG"
    exit 1
fi

# Create the container
docker create --name $BUILDER_IMAGE_NAME $BUILDER_IMAGE_NAME:$IMAGE_TAG
if [ $? -ne 0 ]; then
    echo "Failed to create $BUILDER_IMAGE_NAME:$IMAGE_TAG"
    exit 1
fi

# Copy the build output into the Docker host
docker cp $BUILDER_IMAGE_NAME:/app/HelloWorld.jar ./HelloWorld.jar

# Clean up the container
docker rm $BUILDER_IMAGE_NAME
docker rmi $BUILDER_IMAGE_NAME:$IMAGE_TAG

# Build the actual image
docker build -t $IMAGE_NAME:$IMAGE_TAG -f Dockerfile .
Enter fullscreen mode Exit fullscreen mode

This script automates the creation and deployment of a Dockerized application named "welcomeapp." Here's how it works:

  1. Setup: It defines names and tags for the Docker images involved: one for building (BUILDER_IMAGE_NAME) and one for the final application (IMAGE_NAME).
  2. Builder Image Creation: It builds a builder image using a specific Dockerfile (Dockerfile.build), presumably containing instructions for compiling the application. It cleans up any previous build artifacts before starting and checks for build errors, stopping if any occur.
  3. Artifact Extraction: A temporary Docker container is created from the builder image. This container is used to copy the compiled application files (from /app/) out of the container and into the host system's file system.
  4. Cleanup: The script tidies up by removing the temporary container and deleting the builder image, as they're no longer needed.
  5. Application Image Creation: Finally, it builds the actual application image (IMAGE_NAME) using a different Dockerfile, likely incorporating the extracted application files. This image is ready for deployment and running the "welcomeapp."

In a nutshell, this script simplifies the whole process by dividing the build into two steps (builder and application), ensuring a clean and efficient way to package and deploy the "welcomeapp" in a Docker container.


Add execution permissions to the build.sh shell script

chmod +x build.sh
Enter fullscreen mode Exit fullscreen mode

Now that you have the two Dockerfiles and the shell script, build the Docker image by executing the build.sh shell script

./build.sh
Enter fullscreen mode Exit fullscreen mode

You should get the following output:

++ IMAGE_NAME=helloapp
++ BUILDER_IMAGE_NAME=helloapp-builder
++ IMAGE_TAG=latest
++ echo 'Building helloapp-builder:latest'
Building helloapp-builder:latest
++ rm -rf target
++ docker build -t helloapp-builder:latest -f Dockerfile.build .
#0 building with "default" instance using docker driver

#1 [internal] load .dockerignore
#1 transferring context: 2B done
#1 DONE 0.0s

#2 [internal] load build definition from Dockerfile.build
#2 transferring dockerfile: 344B done
#2 DONE 0.0s

#3 [internal] load metadata for docker.io/library/openjdk:latest
#3 ...

#4 [auth] library/openjdk:pull token for registry-1.docker.io
#4 DONE 0.0s

#3 [internal] load metadata for docker.io/library/openjdk:latest
#3 DONE 3.6s

#5 [1/5] FROM docker.io/library/openjdk:latest@sha256:9b448de897d211c9e0ec635a485650aed6e28d4eca1efbc34940560a480b3f1f
#5 DONE 0.0s

#6 [internal] load build context
#6 transferring context: 169B done
#6 DONE 0.0s

#7 [4/5] RUN javac HelloWorld.java
#7 CACHED

#8 [2/5] WORKDIR /app
#8 CACHED

#9 [3/5] COPY HelloWorld.java /app
#9 CACHED

#10 [5/5] RUN jar cvf HelloWorld.jar HelloWorld.class
#10 CACHED

#11 exporting to image
#11 exporting layers done
#11 writing image sha256:2d5900c70ddee5681fafde163931dc1536f6812e2caeccbc172794b13c99552b done
#11 naming to docker.io/library/helloapp-builder:latest done
#11 DONE 0.0s

What's Next?
  View a summary of image vulnerabilities and recommendations → docker scout quickview
++ '[' 0 -ne 0 ']'
++ docker create --name helloapp-builder helloapp-builder:latest
bf4df7ae3e2fed2dc97b2f1952656819c87e37454108c7ef6a9e3747895f0adb
++ '[' 0 -ne 0 ']'
++ docker cp helloapp-builder:/app/HelloWorld.jar ./HelloWorld.jar
++ docker rm helloapp-builder
helloapp-builder
++ docker rmi helloapp-builder:latest
Untagged: helloapp-builder:latest
Deleted: sha256:2d5900c70ddee5681fafde163931dc1536f6812e2caeccbc172794b13c99552b
++ docker build -t helloapp:latest -f Dockerfile .
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 243B done
#1 DONE 0.0s

#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.0s

#3 [internal] load metadata for docker.io/library/openjdk:11-jdk-slim
#3 DONE 0.8s

#4 [1/3] FROM docker.io/library/openjdk:11-jdk-slim@sha256:868a4f2151d38ba6a09870cec584346a5edc8e9b71fde275eb2e0625273e2fd8
#4 DONE 0.0s

#5 [2/3] WORKDIR /app
#5 CACHED

#6 [internal] load build context
#6 transferring context: 798B done
#6 DONE 0.0s

#7 [3/3] COPY HelloWorld.jar /app/HelloWorld.jar
#7 DONE 0.0s

#8 exporting to image
#8 exporting layers 0.0s done
#8 writing image sha256:73f70ee8012a258801314c0280f1b2788afbffbf57c6235a09f00e30406e3404 done
#8 naming to docker.io/library/helloapp:latest done
#8 DONE 0.0s
Enter fullscreen mode Exit fullscreen mode

Use the docker image ls command to list all the Docker images available

docker image ls
Enter fullscreen mode Exit fullscreen mode

You should get a list of all the available Docker images as shown below

REPOSITORY                                      TAG       IMAGE ID       CREATED              SIZE
helloapp                                        latest    73f70ee8012a   About a minute ago   274MB
Enter fullscreen mode Exit fullscreen mode

As you can see the image created has a size of 274MB, while our previous image had a size of 470 MB.

While the builder pattern is a handy way to slim down your Docker images, it does come with some overhead. You have to juggle two Dockerfiles and a shell script to make everything work smoothly. Fortunately, there's a more streamlined approach: multi-stage Dockerfiles.

In the next section, we'll ditch the multiple files and shell scripts and explore how multi-stage Dockerfiles can simplify the process while still achieving the same goal of creating leaner and more efficient images. Get ready to discover a simpler and more elegant way to optimize your Docker builds.

Introduction to Multi-Stage Dockerfiles

Multi-stage Dockerfiles offer a streamlined way to create optimized Docker images within a single file. They achieve this by incorporating multiple stages, each potentially using a different base image and serving a specific purpose. Similar to the builder pattern we explored earlier, these stages typically include a builder stage for compiling source code and a runtime stage for executing the resulting application.

The key difference lies in the use of multiple FROM directives within a single Dockerfile, where each directive marks the beginning of a new stage. This allows for greater flexibility and efficiency, as you can selectively copy only the essential files from one stage to another, leaving behind any unnecessary build tools or dependencies.

Before the introduction of multi-stage builds, the builder pattern was the go-to method for achieving this optimization. However, multi-stage builds offer a more elegant and concise solution within a single Dockerfile, eliminating the need for multiple files and external scripts.

Multi-stage Docker builds offer a significant advantage over the builder pattern by allowing you to create equally small and efficient Docker images without the hassle of managing multiple files and scripts.

While the builder pattern requires maintaining two Dockerfiles and a shell script, multi-stage builds consolidate everything into a single Dockerfile, simplifying the process considerably. Moreover, the builder pattern necessitates copying executables to the Docker host as an intermediate step before incorporating them into the final image. This extra step isn't needed with multi-stage builds, as you can directly transfer files between different stages within the same Dockerfile using the --from flag.

In essence, multi-stage builds eliminate the extra work and potential for errors associated with the builder pattern, making it a more streamlined and efficient approach to creating optimized Docker images.

The key distinction between a regular Dockerfile and a multi-stage Dockerfile is the use of multiple FROM directives. Each FROM statement kicks off a new phase or stage in the build process, starting with a fresh base image. This means that each stage is isolated from the previous one, except for the specific files or directories you explicitly copy over.

Think of it like building a house in stages. You start with a foundation (the base image), then build the frame, add walls, wiring, plumbing, and so on. Each stage builds upon the previous one, but you don't need all the tools and materials from earlier stages once you've moved on to the next.

In a multi-stage Dockerfile, you use the COPY --from=n instruction to selectively copy the necessary files (like your compiled application) from one stage to another. The number 0 refers to the first stage (stages are numbered starting from 0). This allows you to build your application in one stage with all the necessary tools and dependencies, and then create a leaner final image in a later stage that only contains the essential runtime components.

In a multi-stage Dockerfile, you can refer to each stage by a number, starting from 0. However, to make your Dockerfiles easier to read and maintain, it's often a good practice to give each stage a meaningful name.

You can do this by adding the AS keyword after the FROM directive, followed by the name you want to give the stage. For example:

FROM node:18 AS build
# ... build stage instructions
FROM nginx:alpine AS runtime
# ... runtime stage instructions
Enter fullscreen mode Exit fullscreen mode

In this example, we've named the first stage build and the second stage runtime. This makes it clearer what each stage is for and can be helpful when you're referencing stages later in the Dockerfile.

When working with multi-stage Dockerfiles, you might not always need to build all the way to the final stage. Let's imagine you have a Dockerfile with two stages:

  1. Development Stage: Packed with all the build and debugging tools you need for coding and testing.
  2. Production Stage: A leaner version that only includes the necessary runtime tools for deployment.

During development, you might only want to build up to the development stage to test your code and catch any bugs. In this case, the --target flag in the docker build command becomes your best friend. It lets you specify which stage should be the final one for the resulting image.

For example, if you want to stop at the development stage, you'd run:

docker build --target developement -t my-image:dev .
Enter fullscreen mode Exit fullscreen mode

Here, development is the name we gave to the development stage in the Dockerfile, and my-image:dev is the name and tag we're giving to the resulting image.

This way, you can create multiple images from a single Dockerfile, each tailored to a specific purpose in your development workflow.

Building a Docker Image with a Multi-Stage Docker Build

In this example, we are going to create a Go application using a Multi-Stage Docker Build. Create a new directory named multi-stage-build for this example:

mkdir multi-stage-build
Enter fullscreen mode Exit fullscreen mode

And navigate to it:

cd multi-stage-build
Enter fullscreen mode Exit fullscreen mode

Create the following hellodocker.go file

package main

import "fmt"

func main() {
    fmt.Println("Hello, Docker! I am built using a multi-stage build.")
}
Enter fullscreen mode Exit fullscreen mode

Now create the multi-stage Dockerfile

FROM golang:latest AS builder
WORKDIR /app
COPY hellodocker.go .
RUN go build -o hello hellodocker.go

FROM scratch
WORKDIR /app
COPY --from=builder /app/hello .
ENTRYPOINT ["./hello"]
Enter fullscreen mode Exit fullscreen mode

This Dockerfile employs a multi-stage build process, optimizing the resulting image size. It begins with a builder stage, leveraging the golang:latest image to create an environment suitable for compiling Go code. Within this stage, the hellodocker.go source file is copied into the /app directory, and the Go compiler (go build) generates an executable named hello.

The subsequent stage uses the scratch image, a bare-bones image devoid of an operating system, as its foundation. This minimalistic environment is ideal for running the compiled application. The hello executable, created in the previous builder stage, is copied into the /app directory of this scratch image, ensuring that only the essential binary and its runtime dependencies are included.

Finally, the ENTRYPOINT ["./hello"] instruction dictates that upon container creation, the hello binary will be executed, starting the application. This multi-stage approach effectively minimizes the final image size, making it more efficient for deployment and execution.


Build the Docker image using the following command

docker build -t hello-multistage:v1
Enter fullscreen mode Exit fullscreen mode

You should get an output similar to the following

#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 233B done
#1 DONE 0.0s

#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.0s

#3 [internal] load metadata for docker.io/library/golang:latest
#3 ...

#4 [auth] library/golang:pull token for registry-1.docker.io
#4 DONE 0.0s

#3 [internal] load metadata for docker.io/library/golang:latest
#3 DONE 5.1s

#5 [internal] load build context
#5 DONE 0.0s

#6 [stage-1 1/2] WORKDIR /app
#6 DONE 0.0s

#7 [builder 1/4] FROM docker.io/library/golang:latest@sha256:829eff99a4b2abffe68f6a3847337bf6455d69d17e49ec1a97dac78834754bd6
#7 resolve docker.io/library/golang:latest@sha256:829eff99a4b2abffe68f6a3847337bf6455d69d17e49ec1a97dac78834754bd6 0.0s done
#7 ...

#5 [internal] load build context
#5 transferring context: 159B done
#5 DONE 0.0s

#7 [builder 1/4] FROM docker.io/library/golang:latest@sha256:829eff99a4b2abffe68f6a3847337bf6455d69d17e49ec1a97dac78834754bd6
[...truncated...]

#8 [builder 2/4] WORKDIR /app
#8 DONE 1.0s

#9 [builder 3/4] COPY hellodocker.go .
#9 DONE 0.0s

#10 [builder 4/4] RUN go build -o hello hellodocker.go
#10 DONE 3.7s

#11 [stage-1 2/2] COPY --from=builder /app/hello .
#11 DONE 0.1s

#12 exporting to image
#12 exporting layers 0.1s done
#12 writing image sha256:6ff5d8bfd14ee1893ea5d1e315aa8c394edbc879369cd3d59326dd6256184625 done
#12 naming to docker.io/library/hello-multistage:v1 done
#12 DONE 0.1s

What's Next?
  View a summary of image vulnerabilities and recommendations → docker scout quickview
Enter fullscreen mode Exit fullscreen mode

Use the docker image ls command to list all docker images available on your computer.

docker images ls
Enter fullscreen mode Exit fullscreen mode
REPOSITORY                                      TAG       IMAGE ID       CREATED         SIZE
hello-multistage                                v1        6ff5d8bfd14e   9 minutes ago   1.89MB
Enter fullscreen mode Exit fullscreen mode

In this example we discussed how to use a multi-stage Dockerfile to build optimized Docker images.

Summary

Multi-stage builds, introduced in Docker version 17.05, aim to streamline the Docker image creation process by using multiple stages within a single Dockerfile. Each stage focuses on specific tasks and allows for selective copying of artifacts between stages, ultimately optimizing the final image size and efficiency.

Before multi-stage builds, we often relied on the builder pattern, which involved using separate Dockerfiles and shell scripts to achieve similar optimization goals. While effective, this approach added complexity to the build process.

In this post, we started by discussing the challenges of traditional Docker builds, especially in production environments where image size and build efficiency are critical. Then, we introduced the builder pattern as a precursor to multi-stage builds, explaining its use of two Docker images—one for building and one for runtime—with selective artifact copying between them.

Finally, we delved into multi-stage Dockerfiles as a more integrated solution. Here, you define multiple stages within a single Dockerfile using FROM directives. Each stage builds upon the previous one but allows you to copy only the necessary files, eliminating the need for multiple Dockerfiles and external scripts. This approach simplifies the build process while ensuring smaller, more efficient Docker images for deployment.

Overall, multi-stage Dockerfiles offer a streamlined and elegant solution for optimizing Docker image builds, reducing complexity while enhancing efficiency—a significant benefit for anyone managing Docker images in production environments.

Top comments (0)