Multi-stage docker pattern for Golang

Sagar Jauhari
3 min readSep 21, 2018

--

Docker’s multi-stage builds feature allows you to create multiple images in the same Dockerfile. This removes the need to create multiple dockerfiles (one for each image) and helps us in DRYing our configuration by using artifacts from one image in another.

One of the patterns that I have found useful is to map the development stages (test, development, stage, production, etc) in Dockerfile and docker-compose.yml. By doing this you can tell Docker about your various environments and use it as a workhorse to push your apps through various stages of development and release.

Dockerfile

Lets start with an example of a Dockerfile which utilizes these features:

Test image

  • The test image can be as bulky as needed. It should have all the source code and build dependencies installed — basically everything you need to be able to run ‘go test’.
  • Building the actually Go binary is slow. So, the test container should not be responsible for building the production binary — this way the tests can be run quickly each time you make changes to your source files by just running ‘go test’.

To learn more about testing for Golang, try ‘go help testfunc | less’ .

Development image

At this point, you already have the test image and you’ve tested your app using the ‘go test’. Now is the time to use the ‘go build’ command.

  • This is where the binary is actually compiled for development and production.
  • The nice thing about this image is that you can install other tools like bash, ssh, curl, etc to debug your Golang binary in development mode.
  • If this was a web server (for example Echo), you could start a local development server and make web requests to it.

Production image

This is the most crucial and, yet, the simplest of all. You have tested your application using unit tests and you’ve interacted with your application locally in development mode. Now your application is ready to be thrown into the wild.

  • This image is made as lean as possible. For starters, this only has your compiled Golang binary and base OS needed to run it.
  • This image is surprisingly small and, hence, can be deployed quickly even in slow network conditions.

docker-compose.yml

The Dockerfile we’ve created so far can be run using ‘docker build .’ but we’ll need to tag the images manually to make any sense out of them. Along with other nice features, Docker Compose does tagging automatically so lets also add a ‘docker-compose.yml’ file to our project to easily build and analyze these images.

Mapping multi-stage docker images in a docker-compose file
  • We need ‘v3.4’ to use the multi-stage features.
  • Note how each sub-image docker image is mapped to its own service (test, dev and prod) using docker’s ‘target’ flag
  • We can make use of YAML Anchors to prevent code duplication across services

With the Dockerfile and docker-compose.yml set up, we can work with the 3 images easily:

  • docker-compose build
  • docker-compose run test
  • docker-compose run dev
  • docker-compose run prod

We can also find the sizes of different images:

Size of the docker images

Wow! For our toy application the entire image including the OS layer and the compiled binary is 6.3MB! And this is without any specific size optimizations. You might be able to further reduce the size of the compiled binary using compile-time tweaks.

Conclusion

We can leverage Docker’s multi-stage feature to push our environment specific logic to Docker and Docker Compose. This can give us

  • a blazing fast test environment,
  • a debuggable development environment and
  • a super-lean production environment

Resources

--

--