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.

FROM golang:1.9.7
WORKDIR /app/
COPY main.go /app/
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOPATH=`pwd` go build -o webin
CMD ["./app/webin"]

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.

FROM golang:1.9.7 as builder
WORKDIR /app/
COPY main.go /app/
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOPATH=`pwd` go build -o webin

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/webin .
CMD ["./webin"]  

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.

Categories:

Updated: