Build your Elixir application as a self-contained binary

During the last year, at Kalvad, Elixir has become one of our favourite language to work with. However, there are still some downsides to it and today, we will try to solve one of them: the lack of portability.

This post follows another post demonstrating how to build a CRON with Elixir. I will be using the same project to demonstrate how to set up the application.

Erlang solutions

Erlang being older than Elixir, its ecosystem already has some tools that build a project as a standalone application.  I will present them here but since they do not integrate well in an Elixir application, I won't consider any of those.

Rebar

Rebar3 is probably the best option when working with an Erlang application. It manages the dependency and has a lot of usage for building and releasing Erlang. It also has a command to ship the project as a tarball.

Escript

By default, Escript is a simple tool to execute an Erlang script. However, it also includes a command to ship the script with the Erlang runtime. Therefore building a self-sufficient package.

Elixir solutions

Elixir starts to grow and it now offers some great tools made exactly for what we are trying to do.

Mix release

Since Elixir 1.9, Mix provides the command mix release that releases a self-contained application.

What is great about Mix release

  • Official solution
  • Easy to set up
  • Keep evolving with Elixir
  • Maintained by the Elixir team

What is bad about Mix release

  • Package must be run on the same OS it was built on and requires a target-triple match

Distillery

I can't do a better introduction than the official one:

Every alchemist requires good tools, and one of the greatest tools in the alchemist's disposal is the distillery. The purpose of the distillery is to take something and break it down to its component parts, reassembling it into something better, more powerful. That is exactly what this project does - it takes your Mix project and produces an Erlang/OTP release, a distilled form of your raw application's components; a single package which can be deployed anywhere, independently of an Erlang/Elixir installation. No dependencies, no hassle.

What is great about Distillery

  • Probably the oldest (therefore reliable) tool

What is bad about Distillery

  • Slowly becoming deprecated

Bakeware & Burrito

Bakeware  and Burrito extend the mix releasecommand to cover more use cases and optimization.

What is great about Bakeware & Burrito

  • extend a reliable command
  • provide good optimization
  • support all platforms
  • the repositories are still active

What is bad about Bakeware & Burrito

  • still on early release
  • not easy to setup
  • do not support all platforms yet

Our choice

At first, I wanted to use distillery. It was by far the most mature project, with a lot of good documentation. However, since it is not maintained, I gave up on that one.

Bakeware and Burrito are very promising but since they are not stable yet, I prefer to avoid using them.

Finally, I like when things are simple. mix release allows me to do what I want, its easy to use and since its built by the core team, I am confident about its future.

Let's setup our build

In mix.exs, under the def project function, add a releases block that define your target:

def project do
    [
      app: :omycron,
      version: "0.1.0",
      elixir: "~> 1.12",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      releases: [
        staging: [
          include_executables_for: [:unix],
          applications: [runtime_tools: :permanent]
        ]
      ]
    ]
end

Yes. It is that simple. Let's build our application:

$ MIX_ENV=prod mix release staging

Tada! The release is built and you can now execute it:

_build/prod/rel/staging/bin/staging start

Note you can have many releases, staging is just the name I chose but you can use any name you want as long as you target the right name when running mix release.

Notes & Optimization

Something important to know is that the application must be built with the same OS, same architecture, and the same ABI than the hosting system. This is called a target-triple (x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl, x86_64-apple-darwin.)

On top of that, by default, the release does not handle system packages. But there is an option to also include compiled object files in the release.

Finally, the code can be executed in 2 different modes:

  • interactive: modules are loaded when they are being used for the first time. For example the Enum module will not be loaded into the VM if you are not using it. It's great for optimization but the first request to any module will be longer than usual.
  • embedded: it loads all the modules at runtime.

Erlang/OTP earlier than 23+ only supports embedded. Erlang/OPT older than that do a mix of the 2 by switching to interactive when deploying or configuring and switching back to embedded when everything is ready. Note you can force one mode or another by using the RELEASE_MODE flag.

Final Disclaimer

I want to point out that even though we have solutions to package an Elixir application, we will never have the same portability and performances than a Golang, Crystal or any other compiled language.

I hope you enjoyed this article and I hope to see you soon for some good Elixir content!

If you have a problem and no one else can help. Maybe you can hire the Kalvad-Team.