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
Let̵
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 (example.css
).
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 sass
.
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.
Conclusion
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.
Source link