Prerequisites

This tutorial will need the master build of Docker, which is available on Play With Docker.

Multi-staged builds

A common pipe-line for building applications in Docker involves adding SDKs and runtimes, followed by adding code and building it. The most efficient way to get a small image tends to be to use 2-3 Dockerfiles with different filenames where each one takes the output of the last. This is referred to as the Builder pattern in the Docker community.

This lab explores a new bleeding-edge feature called Multi-stage builds. It is not yet released into a Docker version, but when it is available on the Docker Hub/Cloud and for all the Docker editions it will mean we can use a single Dockerfile with multiple stages instead of the Builder pattern.

Let’s build a simple Golang application which counts internal/external facing anchor tags to help us come up with an SEO rating.

Let’s try out the href-counter Docker image from the hub, then look at how to re-build from the Github repository:

docker run -e url=https://news.ycombinator.com alexellis2/href-counter
{"internal":197,"external":32}

You get a JSON object returned giving the total amount of internal vs external links.

Let’s clone the source:

git clone https://github.com/alexellis/href-counter
cd href-counter
Cloning into 'href-counter'...
remote: Counting objects: 24, done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 24 (delta 7), reused 12 (delta 1), pack-reused 0
Unpacking objects: 100% (24/24), done.

The old way of doing things

Let’s build the Docker image with all the Golang toolchain and see how big the image comes out as:

docker build -t alexellis2/href-counter:sdk . -f Dockerfile.build

Now check the size of the image:

docker images |grep href-counter
docker images |grep href-counter
href-counter        sdk                 131c782e8c35        30 second
s ago      692MB

The docker history command will show you that the layers we added during the build are only a small part of the resulting image (about 20MB +/-):

docker history alexellis2/href-counter:sdk |head -n 4
IMAGE               CREATED             CREATED BY
                   SIZE                COMMENT
f8b1953fb9c7        1 second ago        /bin/sh -c CGO_ENABLED=0 GOOS=linux go bui...   5.64MB
5d24895500e8        9 seconds ago       /bin/sh -c #(nop) COPY file:d3eec1f1fefbec...   1.71kB
d83dc0785057        9 seconds ago       /bin/sh -c go get -d -v golang.org/x/net/html   13.6MB
c6f59b210906        11 seconds ago      /bin/sh -c #(nop) WORKDIR /go/src/github.c...   0B

The image is quite large, but this Golang package can be built into a very small binary with no external dependencies, then added to an Alpine Linux base image.

The builder pattern

Type in cat builder.sh so you can see how the builder pattern uses two separate Dockerfiles. This will help us get the context for the next step where we will use a single Dockerfile.

./build.sh
#!/bin/sh
echo Building alexellis2/href-counter:build

docker build -t alexellis2/href-counter:build . -f Dockerfile.build
docker create --name extract alexellis2/href-counter:build
docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app
docker rm -f extract

echo Building alexellis2/href-counter:latest
docker build -t alexellis2/href-counter:latest .

As you can see there are quite a few intermediate steps required to create an optimized image using the Builder pattern.

Let’s see how big the Docker image came out as:

docker images |grep alexellis2/href-counter

This is much smaller than when we built our first image with the Golang SDK included.

Multi-stage build example

While the builder pattern helps us achieve a small image, it does require extra leg-work for every piece of software we want to package in Docker.

Here is where multi-stage builds help us out. Instead of using a shell script to orchestrate two separate Dockerfiles, we can just use one and define stages throughout.

To use the Dockerfile.multi file in the Github repository to do a multi-stage build, then check the size of the resulting image against that of the image created by the Golang SDK base and the builder pattern.

docker build -t alexellis2/href-counter:multi . -f Dockerfile.multi
docker images |grep href-counter
alexellis2/href-counter   multi               44852229a1cc        2 minutes ago       10.3MB
alexellis2/href-counter   latest              bb997f819fbb        3 minutes ago       10.3MB
alexellis2/href-counter   build               298d3b970412        4 minutes ago       692MB
alexellis2/href-counter   sdk                 298d3b970412        4 minutes ago       692MB
alexellis2/href-counter   <none>              b0a73b688243        13 days ago         11.6MB

Here is an example of building “Hello World” in Go.

Create a new folder:

mkdir hello
cd hello

Create the app.go file:

echo 'package main

import "fmt"

func main() {
    fmt.Println("Hello world!")
}
' | tee app.go

Create a Dockerfile with the following contents:

echo '
FROM golang:1.7.3
COPY app.go .
RUN go build -o app app.go

FROM scratch
COPY --from=0 /go/app .
CMD ["./app"]
' | tee Dockerfile

Now build and run the Dockerfile:

docker build -t hello-world-lab .
docker run hello-world-lab

The resulting size of hello-world is very small:

docker images |grep hello-world-lab

More about the lab

This lab was built from a blog post. by Alex Ellis

Note: We do not recommend moving over to multi-stage builds until they are fully available on the Docker Hub/Cloud and all editions of Docker. The example in build.sh provides an interim solution for using separate Dockerfiles to build and ship code.