Skip to main content

Command Palette

Search for a command to run...

Multi-stage Docker Build: What? Why? Where?

Published
3 min read

Write about the docker multi-stage build, what it means, what it can be used for, and some examples.

An Overview of Multi-stage Docker Builds

Multi-stage Docker builds allows us to build a docker image using multiple parent images, you can start guessing what benefits it has already. One can copy essential binaries from one docker image and move them to another parent image and build a new image that does not include unnecessary binaries from the first image. This way, we can have an optimized docker image.

The FROM instruction specifies the Parent Image from which you are building.

source: https://docs.docker.com/engine/reference/builder/#format

Example of Using Multi-stage Builds

Let's assume we need to build our project, for this example, we will build a CLI that provides the user with weather information. The CLI is open-source and has been built with Go. After building the CLI, we would like to have a docker image that contains it - but only it, without the project itself. We will see how we can do it, and how much space and layers it saves us.

So we will start by cloning the project to our local environment:

git clone https://github.com/arrufat/yandex-weather-cli.git

Create Dockerfile:

cd yandex-weather-cli
touch Dockerfile

Using a text editor, write the following in the Dockerfile we just created:

FROM golang:1.16
WORKDIR /go/src/github.com/arrufat/yandex-weather-cli
COPY *.go go.mod go.sum ./
RUN go mod download
RUN CGO_ENABLED=0 go build
ENTRYPOINT ["./yandex-weather-cli"]

And lastly, build the image:

docker build .

We now have an image that has the CLI bin that can be executed within a container that runs with this image. But - we also have in our image all the project, we have extra layers (for each line of instruction - FROM, COPY, etc...).

A side note: we have some extra layers because we copied files, downloaded go modules, and built the project, these instructions cost us in extra layers :)

So instead of just building the image and keeping everything, we can copy the desired bin (our CLI we just built) to another lightweight parent image. Modify your Dockerfile so it will look like this:

FROM golang:1.16 as builder
WORKDIR /go/src/github.com/arrufat/yandex-weather-cli
COPY *.go go.mod go.sum ./
RUN go mod download
RUN CGO_ENABLED=0 go build

FROM alpine:latest
WORKDIR /app
COPY --from=builder /go/src/github.com/arrufat/yandex-weather-cli/yandex-weather-cli ./
ENTRYPOINT ["./yandex-weather-cli"]

The changes are:

-FROM golang:1.16
+FROM golang:1.16 as builder
 WORKDIR /go/src/github.com/arrufat/yandex-weather-cli
 COPY *.go go.mod go.sum ./
 RUN go mod download
 RUN CGO_ENABLED=0 go build
+
+FROM alpine:latest
+WORKDIR /app
+COPY --from=builder /go/src/github.com/arrufat/yandex-weather-cli/yandex-weather-cli ./
ENTRYPOINT ["./yandex-weather-cli"]

1) We are now naming the first parent image as builder, this allows us later to copy files from it.
2) We've added another FROM instruction so our parent image is now a different one. (we are using alpine because of its size)
3) We are copying the CLI that we built on the builder parent image to the current one by using the --from flag.

The Differences

# Size
docker images --format="{{.Size}}"
903MB
15.5MB

# Number of layers
docker history full-image | wc -l
      19
docker history multi-step-image | wc -l
       6

More from this blog

My Personal Blog

2 posts

My name is Mor and I have been working as a DevOps Engineer for Red Hat for a year and a half. During this time, I have gained a wealth of knowledge and experience that I am excited to share with you.