With multi-stage Docker builds, you can create Docker files with multiple
FROM explanations. This means you can create images derived from different bases, reducing the size of your final build.
Docker images are created by selecting a base image with the
FROM statement. You then add layers to that image by adding commands to your Docker file.
Multi-phase builds allow you to split your Dockerfile into multiple sections. Each stage has its own
FROM statement so you can include more than one image in your builds. Stages are built sequentially and can refer to their predecessors so you can copy the output from one layer to the next.
Multi-phase builds in action
Here’s a multiphase Docker file that includes our full build:
FROM node:14 AS sass WORKDIR /example RUN npm install -g node-sass COPY example.scss . RUN node-sass example.scss example.css FROM php:8.0-apache COPY --from=composer:2 /usr/bin/composer /usr/bin/composer COPY composer.json . COPY composer.lock . RUN composer install --no-dev COPY --from=sass /example/example.css example.css COPY index.php . COPY src/ src
You immediately notice that we have two
FROM statements that split our Dockerfile into two logical sections. The first stage is dedicated to assembling the Sass, while the second focuses on combining everything in the final container.
We use the
node-sass implementation of Sass. We therefore start with a Node.JS base image, within which we install
node-sass worldwide from npm. We then use
node-sass to compile our stylesheet
example.scss in the pure CSS
example.css. The high-level summary of this stage is that we take a base image, run a command and get an output that we want to use later in our build (
The next stage introduces the base image for our application:
php8.0-apache. The last
FROM statement in your Dockerfile defines the image that your containers will run. Our earlier
node image is ultimately irrelevant to our application’s containers – it is used purely as a convenience tool during build.
Then we use Composer to install our PHP dependencies. Composer is PHP’s package manager, but it is not included with the official PHP Docker images. We therefore copy the binary file to our container from the special Composer image.
We had none
FROM statement to do this. Since we are not running commands for the Composer image, we can use the
--from flag with
COPY to refer to the image. Usually,
COPY copies files from your local build context to your image; with
--from and an image name, it will create a new container with that image and then copy the specified file from it.
Later we use Dockerfile
COPY --from again, this time in a different form. At the top we wrote our first
FROM statement as
FROM node:14 AS sass. The
AS clause created a named stage called
We now reference the temporary container created by this phase with
COPY --from=sass. This allows us to copy our built-in CSS to our final image. The rest of the steps are routine
COPY edits, used to get our source code from our local working directory.
Advantages of multiphase builds
Multiphase builds allow you to create complex build routines with a single Docker file. Before they were introduced, it was common for complex projects to use multiple Docker files, one for each phase of their build. These then had to be orchestrated by manually written shell scripts.
With multiphase builds, our entire build system can be contained in one file. You don’t need wrapper scripts to take your project from raw codebase to the final application image. A regular customer
docker build -t my-image:latest . is sufficient.
This simplification also offers opportunities to improve the efficiency of your images. Docker images can grow large, especially if you use a language runtime as a base.
Take the officer
golang image: it’s almost 300MB. Traditionally, you could copy your Go source code to the image and use it to compile your binary file. You then copy your binary file back to your host computer before starting a new build. This one would use a Docker file with a lightweight base image such as
alpine (about 10 MB). You would add your binary back in, resulting in a much smaller image than if you had used the original
golang base to run your containers.
Multi-phase builds make this type of system much easier to implement:
FROM golang:latest WORKDIR /go COPY app.go . RUN go build -o my-binary FROM alpine:latest WORKDIR /app COPY --from=build /go/my-binary . CMD ["./my-binary"]
In eight lines, we managed to establish a procedure that previously required at least three files – one
golang Dockerfile, a
alpine Docker file and a shell script to manage the steps in between.
Multi-phase builds can greatly simplify the construction of complex Docker images. They let you use multiple interconnected build steps that can push output artifacts forward.
The model also promotes construction efficiency. The ease with which you can reference various base images helps developers ensure that the final output is as small as possible. You will benefit from lower storage and bandwidth costs which can be significant when using Docker in a CI / CD system.