One of the key announcements at
Dockercon 2017
was the launch of multi-stage builds. It is available in Docker version
17.05 onwards and is one of most exciting features. In this post, we
will do a quick demo of the feature and then discuss the details.
Setup
One of the quickest way to get our hands dirty with this feature is to
sign up for docker lab at http://labs.play-with-docker.com and add a new
instance. This will spin up a new docker-in-docker container which can
be used for our experimentation. Before going ahead lets ensure docker
version is correct by running command “docker version” .
If the version of docker server is less than 17.05, perform following
steps:
- To download the docker daemon 17.05-dev run following command:
curl -L https://master.dockerproject.org/linux/amd64/dockerd-17.05.0-dev -o dockerd
- Run the following command to overwrite existing dockerd
mv ./dockerd /usr/local/bin/dockerd && chmod +x /usr/local/bin/dockerd
- Kill the dockerd process by running ps -a and* r*un the
following command to start the docker daemon again
nohup /usr/local/bin/dockerd &
- Verify the docker daemon version by running “docker version“
###
###
###
###
###
###
###
###
###
###
Building Image the old way
Clone git repo of sample go-lang
application and cd to
the code directory. We will now build an image of the demo app via the
default go-lang parent as a base image using the command “docker build
-t demo-go-app:1.0 .” One of the key things to notice here is the
content of Dockerfile, which is simple golang image’s on-build tag:
FROM golang:1.6.3-onbuild
Once the image is built, verify the image size using command “docker
images” and image is about 756MB
###
###
###
###
World of Multi-stage builds
Now check out the multi-stage branch of the same repo. One of the first
things you will notice is starkly different Dockerfile:
FROM golang:1.6.3-alpine
RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
FROM alpine:latest
WORKDIR /root/
COPY --from=0 /app/main .
CMD ["/root/main"]
There are two FROM directives in this file which was not allowed before.
With multi-stage builds, a Dockerfile allows multiple FROM directives
and the image is created via the last FROM directive of the
Dockerfile
Next interesting statement is the “COPY –from=0 /app/main .” This
takes the file /app/main from the previous stage and copies it to the
WORKDIR. This basically copies the compiled go binary created from the
previous stage
Now lets run the following docker build command “docker build -t
demo-go-app:2.0 .” and verify the size by running “docker images”
again
The image size is around 13.1 MB which is a fraction of the older size
756MB
A word of caution:
If the build product is a compiled binary, a consistent platform has to
be maintained. A binary compiled for linux will not work on alpine. For
example, below are the file attributes of above application when it is
built via golang:1.6.3-onbuild
root@44fee7671056:/go/src/app# file /go/bin/app
/go/bin/app: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped
Looking at the same application when compiled via golang:1.6.3-alpine
$ docker exec -it c7a6afccf5509c2032444f14906cf17267994a3191e5d368fad9cff679797377 file /root/main
/root/main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
If the application compiled via golang:1.6.3-onubild gets copied to
alpine container, the application wont execute and will give an error as
below:
$ ./app
bash: ./app: cannot execute binary file: Exec format error
This is why the Dockerfile used in multi-stage build of the demo
application is using golang:1.6.3-alpine as its base image instead of
golang:1.6.3-on-build.
Why is the multi-stage image so small?
Main reason for the image being so small, is that its parent image
itself is very small. While the golang:1.6.3 image was 747MB, the alpine
image was a mere 3.99 MB. Why are alpine images so small and their
advantages are explained in detail
here. The golang
base image contains golang installation + python installation and many
supporting modules, which are not really needed once the compiled
application is ready.
Consider another approach where you would use alpine as a base image and
create an application over it, this would mean adding every little
dependency by hand to your Dockerfile and adding more steps to image
creation which is certainly not desirable.
A third option is to use the alpine distribution of golang such as
golang:1.6.3-alpine. This also creates an image of around 300 MB since
the OS is barebones but all golang dependencies and support modules are
present.
Multi-stage builds solve above problems by giving the developer the
flexibility to use bloated base images to create his executable/jar
file. Once the application is compiled, remaining bloatware is not
needed and the application itself can be easily injected into a minimal
image giving all advantages of small image size.
Looking for help with your cloud native journey? do check our cloud native consulting capabilities and expertise to know how we can help with your transformation journey.