When one develops a product its often very attractive to put everything in a container. Imagine you are developing prototype on some mid size server which is common case for initial stages of development. One would like to have not just product running inside but also testing facilities.

Slowly you pile more things onto your dev machine and your initial build stops being as nice citizen of the server as it used to be. It quickly gets too big espetially if you keep several versions of it. You never wanted to deliver it in that size in the first place. So that is the point where one can leverage multistage build. That is about time to start thinking about how it will run in production.

At that stage one can transition on using one docker image to build the app. Second image that does not have build tools involved to just run the app.

Lets look at an example how big of a benefit that is. If you develop a modern website lets say you combine rails and react into one app. With all those dependencies and extra files your build image can be huge just remember how long does it take to install node modules.

Now once you compile those it is significantly leaner together with production version of rails app.

Enter mulsti stage build.

Lets use example from docker website

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]  

So line FROM alpine:latest means now switch to my lean production image. At this point we are not using old intermediary containers from before but we can refer to them and copy stuff they made.

Here is how: line COPY --from=0 /go/src/github.com/alexellis/href-counter/app . means take what build produced in app folder and copy it to this new container. Its copy paste from intermediary container while building initial image to new one we are building.

Now once build completes we have much leaner app. What is a fine pattern is to put first 3 lines into separate docker file. That way you build a build image so you don’t have to install your build tools all the time. Then line 4 copies your current app from disk into docker and you compile it that can also be separate docker file and separate stage. You just saved all that time it took you to install all the tools. Then what is left is just to use FROM myproductionimage together with COPY stuffThatBuildOutputed toAppFolderInLeanContainer.

What benefits did we get.

  • Separation of concerns when it comes to build tools and app itself.
  • Faster builds if we build bajilion times a day since most often we don’t change dependencies all that often.
  • If we add dependency no problem our build system will take bit longer to build images we use for building.
  • If we grab that build output and put it into our testimage we just used that sweet sauce to prepare our testing and we have in our test environment same thing that runs on production or pretty close.
  • Size is much smaller if you keep multiple versions that does not need mentioning and tests take smaller space too.

Sometimes if multistage build gets stubborn and you see errors like you can’t build container due to some intermediory step failing just use RUN true as a step where it fails. That will create empty intermediate container that will get us some “traction” :-D.

Happy hacking.