Stacks on Stacks

Docker Multi-stage Builds for Interpreted Languages

Docker mutli-stage builds are great for compiled languages, but what about interpreted languages that require the source code at runtime? In this post, we’ll see how to use Docker multi-stage builds with interpreted languages to create lean production images while still being easily able to create debug/testing images, all from a single Dockerfile.

Compiled Languages

Docker multi-stage builds are a dream come true for compiled languages, such as golang. You can install all the build tools you might need in a builder image, compile your binary/binaries, and then copy them into a final production image that has a very small footprint. This makes it efficient to push your image and ,subsequently, to pull your image for local development or to pull your image on something like kubernetes. Let’s look at this pattern for something like golang:

The first image we build copies in all of the source code and compiles it to a single binary. The second image, which will be our production image, is the alpine base image with our binary copied in and set as the default ENTRYPOINT.

InterpretedLanguages

The pattern I’ve found for interpreted languages will be a bit differnt. We’ll first start by copying in only the things needed to run our application in production in the first stage. Then, we’ll create a second stage which copies in all of our testing & debugging tools.

For this example, we’ll be using an Ember app to demonstrate:

Here we have a little bit more going on and we can see that the pattern is inverted. Installing Google Chrome is great for running our ember tests, but we probably don’t want it included in every single artifact we are building. This would only serve to increase the size of all of our deployable images and increase pull/deploy times. However, it might be beneficial to install these in something like a CI system to verify our code. There are two things to note in this pattern though:

  1. If you build the docker image as you might expect with the compiled language, e.g. docker build -t my-image ., what you will end up with is the testing image. For that reason, you might want to tag the testing image as such, with: docker build -t my-image:test .
  2. To build the production image, you’ll actually want to run docker build --target production -t my-image . Once you get used to this, its not too bad to do this in a CI environment. You actually build the production image first and tag it, and then build the test image. Docker is smart enough to use the build cache and only needs to add the tests steps.