DEV Community

Cover image for Multi-stage Docker and Elixir releases
Tom Nicklin
Tom Nicklin

Posted on

Multi-stage Docker and Elixir releases

Background

Last time I was trying to make a serious elixir app was in v1.4. All I can remember about it was that it was hell. So what's changed? In v1.9 we got "releases" which makes deployment a breeze. At the time of writing, in v1.10, there have been further improvements to this.

For my new project, I just want some simple backend APIs and to manage them in a Kubernetes cluster, so it's time to readdress Elixir and Docker. I personally couldn't find exactly what I wanted online and went through some grief trying to accomplish my goals.

My aims were simple:

  • An API that returns JSON
  • A small container for said API

For the first goal, I struck gold. Jon Lunsford has a great guide for this.


It's served as a great jumping-off point. Although, I chose Jason for my JSON parser, instead of Poison - as it's been shown that it's at least twice as fast.

If you are following along, I would recommend looking over the article embedded above.


Dockerfile

In the above Dockerfile, let's use the latest alpine version of elixir. On lines 3-5 we COPY exactly what we need and no more. Then we build the API, and ultimately serve it up ready for requests. How are we doing on the size of the image?

dockerfile size
103MB not bad for an OTP application. But, can we improve that?


Multi-stage Dockerfile

Some, if not a lot of that size, is down to the building process and the required toolchain. Multi-stage builds with Docker can be used to eliminate that extra space. Once the build for the app has been done, we just need the release binaries.

Now, let's re-jig the docker file into 2 separate parts: a build stage and an application stage. In the first line, you can see that the same base image has been used, but more importantly, as builder.

In this part, we don't really care about space used, because it's all going to be removed and only the application stage counts towards the file size of the image. So, we could just as easily COPY . for the whole directory; it would affect the build context, resulting in slower builds but you get the point. We have much of the same in the building section, copying just what we need and compiling the release binaries.

In the application stage, we choose alpine again. But, just the OS, we don't need the version that comes with elixir. On line 14, we will still need the tools in order to run the release the way we want. On line 16 is where it gets juicy. We just copy the release binaries that we created from the build stage and then serve them up as we did in the earlier docker file.

Now when we build, docker build -t my-app:latest .

Multi-stage docker

Less than 20MB!


Testing

Let's give this a test.

For ease of use in the future, I normally create a script to build and run my images.

Normally, as I only want to do a quick test locally, I like to run in foreground mode so it's easier to see the logs; and with --rm so I can just ctrl+c out and the container cleans up after itself.

events

And hey presto, we have a small JSON API running in a tiny image.

Thanks for reading ❤️

Oldest comments (3)

Collapse
 
alessie77 profile image
alessie77

Really interesting read!

Not too long and very informative. Will be subscribing to you after this.

Collapse
 
sirensi profile image
sirensi

Found this so helpful. Thanks!

Collapse
 
quatermain profile image
Oliver Kriška

Nice article. I would like to suggest to show people also possibility to use even more stages. Nice example is using another Docker stage for building assets in case of Phoenix framework. It's easy and just a few lines of code. For example:

...
FROM node:10.16-alpine AS assets

WORKDIR /app
# Compile assets

COPY --from=builder /app/deps /app/deps

COPY assets/ /app/assets/
RUN cd /app/assets && npm install && npm run deploy

FROM builder AS release

COPY --from=assets /app/priv/static /app/priv/static
...