Multi-stage Docker and Elixir releases
Tags: elixir, docker, microservices
Published: 2020-04-03 02:21:26

#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](https://github.com/elixir-lang/elixir/releases/tag/v1.9.0) 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](https://dev.to/jonlunsford) has a great guide for this. {% link https://dev.to/jonlunsford/elixir-building-a-small-json-endpoint-with-plug-cowboy-and-poison-1826 %} 
It's served as a great jumping-off point. Although, I chose [Jason](https://hexdocs.pm/jason/readme.html) for my JSON parser, instead of Poison - as it's been shown that it's [at least twice as fast](https://gist.github.com/michalmuskala/4d64a5a7696ca84ac7c169a0206640d5).

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

---

# Dockerfile
{% gist https://gist.github.com/shmink/d4812d9e29567895eea611bfc8a21278 %}

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](https://i.imgur.com/93Sbe23.png)
103MB not bad for an [OTP](https://elixirschool.com/en/lessons/advanced/otp-concurrency/) 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](https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds) can be used to eliminate that extra space. Once the build for the app has been done, we just need the release binaries.

{% gist https://gist.github.com/shmink/57e5f23b4016de85ba76e8c3e1cd4da4 %}

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](https://i.imgur.com/SHtqwRn.png)

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. 

{% gist https://gist.github.com/shmink/b5b4ebae3ef423847d62aa3555a4a6fc %}

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 <kbd>ctrl</kbd>+<kbd>c</kbd> out and the container cleans up after itself. 

![events](https://i.imgur.com/onvAuwS.png)

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

Thanks for reading ❤️