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 release
command 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.