Docker - using multistage build
Have you ever tried building code on Docker just to end up with a huge container? Yes? Me too.
I’ll show you the beauty of multistage builds that will enable you to get a result such as this one:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
kstaykov/webin latest 22016f6268d9 12 minutes ago 10.8MB
golang 1.9.7 f9ff4369deb0 2 days ago 750MB
alpine latest 3fd9065eaf02 5 months ago 4.15MB
$
Notice how big the Golang image is. I’m using it to build my simple Go app but then I’ll host the app on a small alpine image which is only 4.15 MB. In the end my image is just 10.8 MB which is the alpine + my built go binary.
We’ll be reviewing the code in this repo here: https://gitlab.com/kstaykov/webin
If you have a look at the main.go you’ll see a very simple Go program that prints http headers and some form info to console for debug purposes. I needed something like that to debug web hooks so I put those few lines of code. The problem however is that I need this tool to be very small and easy to grab at some dev machines that have docker installed.
The straight forward approach would be to make one Dockerfile using Golang image and host my code and build there. That would work. Let’s do it.
And away it goes.
$ docker build -t kstaykov/webin:v1 .
Sending build context to Docker daemon 6.649MB
Step 1/5 : FROM golang:1.9.7
---> f9ff4369deb0
Step 2/5 : WORKDIR /app/
Removing intermediate container 79f8edde1bee
---> f4f165b3523f
Step 3/5 : COPY main.go /app/
---> 29ea023add77
Step 4/5 : RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOPATH=`pwd` go build -o webin
---> Running in f6088791512f
Removing intermediate container f6088791512f
---> e90fa77e9fcf
Step 5/5 : CMD ["./app/webin"]
---> Running in 400db33ba75f
Removing intermediate container 400db33ba75f
---> 830a41c49231
Successfully built 830a41c49231
Successfully tagged kstaykov/webin:v1
$
Yey, it works!
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
kstaykov/webin v1 830a41c49231 2 minutes ago 756MB
golang 1.9.7 f9ff4369deb0 2 days ago 750MB
alpine latest 3fd9065eaf02 5 months ago 4.15MB
$
Oh, wait. The end product image is 756 MBs in size. That’s not nice. It has everything in there. When I was studying docker I heard someone comparing this to a car factory. Using that analogy what we just did is make a car but the whole factory is still attach to it. That won’t sell well so let’s fix this.
We’ll tune our Dockerfile and build our multistage magic.
Beautiful. What we did is very simple. We started with our big Golang image and build the app there. We called that image builder in this small pipeline. Down below we use the binary /app/webin from the builder image which is the end result from the go compilation. That’s how we build the alpine based container with just the binary instead of the whole golang (factory? :P) data.
Away that goes.
$ docker build -t kstaykov/webin:latest .
Sending build context to Docker daemon 6.649MB
Step 1/9 : FROM golang:1.9.7 as builder
---> f9ff4369deb0
Step 2/9 : WORKDIR /app/
Removing intermediate container 5a16bfd5407d
---> d2e0b5d2a598
Step 3/9 : COPY main.go /app/
---> 13ea9e625f06
Step 4/9 : RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOPATH=`pwd` go build -o webin
---> Running in 52d6e844b44e
Removing intermediate container 52d6e844b44e
---> 284f90cd0b4e
Step 5/9 : FROM alpine:latest
---> 3fd9065eaf02
Step 6/9 : RUN apk --no-cache add ca-certificates
---> Running in 92499ce9fbc9
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/community/x86_64/APKINDEX.tar.gz
(1/1) Installing ca-certificates (20171114-r0)
Executing busybox-1.27.2-r7.trigger
Executing ca-certificates-20171114-r0.trigger
OK: 5 MiB in 12 packages
Removing intermediate container 92499ce9fbc9
---> f08ed1ee0649
Step 7/9 : WORKDIR /root/
Removing intermediate container 2bf26a80d6f9
---> 4d8b3c4e0e4a
Step 8/9 : COPY --from=builder /app/webin .
---> db3207ccbe20
Step 9/9 : CMD ["./webin"]
---> Running in fa35ded63440
Removing intermediate container fa35ded63440
---> 76887e697c9f
Successfully built 76887e697c9f
Successfully tagged kstaykov/webin:latest
$
Aaand the resulting image is… OK, you already saw that at the beginning so no surprise here.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
kstaykov/webin latest 76887e697c9f About a minute ago 10.8MB
<none> <none> 284f90cd0b4e 2 minutes ago 756MB
golang 1.9.7 f9ff4369deb0 2 days ago 750MB
alpine latest 3fd9065eaf02 5 months ago 4.15MB
$
I still keep the intermittent 756 MB image just to show the huge difference. Now that 10.8 MB image looks kind of sweet and that’s our whole app within a very small container. A micro service if you will.
In the Java world that would be a maven/gradle build and the resulting image will run a smaller jdk container. Sweet.