Mastering Elixir Releases with Distillery — A (Pretty) Complete Guide

How to create Elixir Releases. And some tips for Phoenix, Ecto, and umbrella applications.

Philipp
Philipp
Sep 12, 2017 · 10 min read

A Quick Refresher and a Bit of History

I have previously written about what Releases are and why they’re great in more detail. Here is a little refresher: Releases are self-contained bundles of compiled Elixir/Erlang code that can easily be deployed on remote machines.

Naturally, most tools for creating Releases have so far come from the Erlang world. Erlang ships with systools and reltool, but their usage is rather complicated. So the community built relx as an easier-to-use alternative that has now become the de-facto standard for creating Releases from Erlang projects. Since those tools are all made to work with Erlang, they can work with Elixir projects but leave much to be desired.

In 2014, Paul aka bitwalker set out to make Elixir Releases easier and published exrm — built on top of relx with some Elixir-specific features. Due to limitations of relx, exrm was inflexible and never able to leverage the full potential of modular Elixir applications. So Paul decided to create a new Release manager, written from scratch in pure Elixir — and so Distillery was born.

Step-by-step guide

I have created a small application that prints out the current time every second. We can use it to build a Release:

git clone https://github.com/wmnnd/distillery-demo.git
cd distillery-demo

Installing Distillery & Creating the First Release

Next, we initialize Distillery by calling mix release.init. This command creates a configuration file rel/config.exs. You could also manually create the file but it’s more convenient like this.

We will look at the contents of the configuration file in a bit. For now it’s enough to build our first Release. In order to do that, run mix release. Distillery will now create an output that looks something like this:

==> Assembling release..
==> Building release clock:0.1.0 using environment dev
==> You have set dev_mode to true, skipping archival phase
==> Release successfully built!
You can run it in one of the following ways:
Interactive: _build/dev/rel/clock/bin/clock console
Foreground: _build/dev/rel/clock/bin/clock foreground
Daemon: _build/dev/rel/clock/bin/clock start

Congratulations, you have just created your first Release! Run it with _build/dev/rel/clock/bin/clock foreground. As expected, the application now prints out the current time in one-second intervals. You can terminate the application by hitting Ctrl+C.

Note: There is currently a bug in Distillery that might crash the Release when starting it. In this case, run mix clean && mix compile and try running the application again.

In the call to start the application, you passed the foreground command at the end. This is the boot command. There are also other boot commands for starting an application: console launches an interactive iex session with your application and start creates a daemon process in the background.

Release Profiles and Environments

You might have noticed that when we created the Release, Distillery printed out this line: Building release clock:0.1.0 using environment dev. Distillery also has environments — but they are different from Mix environments. Distillery uses environments to allow for multiple configurations of the Release building process.

Conveniently — or confusingly — Distillery environments are by default the same as the Mix environments. Let’s take a look at the auto-generated configuration file rel/config.exs in order to better understand how profiles work:

use Mix.Releases.Config,
default_release: :default,
default_environment: Mix.env()

We can see that default_environment is set to Mix.env(). This means that Distillery will match our Mix environment. You can override default environment with the --env switch. Here are some examples of how this could work:

mix release
#Mix environment and Distillery environment are both dev
MIX_ENV=prod mix release
#Mix environment and Distillery environment are both prod
MIX_ENV=prod mix release --env=dev
#Mix environment is prod, Distillery environment is dev

The configuration file also specifies that default_release is :default. This is because you can actually define more than just one Release for your project. With the :default setting, Distillery automatically picks the first Release defined in rel/config.exs. If you want to override the default Release, you can specify this with the --name flag.

The combination of an environment with a specific Release name is called a profile. You can pick a specific profile with the --profile=name:env flag.

In the next section we’re going to take a look at how we can use multiple environments to configure Distillery.

Release Build Configuration

environment :dev do
set dev_mode: true
set include_erts: false
set cookie: :"W?cN_`G<>ayUI&ku{<$3w7J<^nUBRBu[F[…]"
end
environment :prod do
set include_erts: true
set include_src: false
set cookie: "^_`fz{dk|`w.n3Z%T,n=F>ezazFk.1ci5}[…]"
end
release :clock do
set version: current_version(:clock)
set applications: [
:runtime_tools
]
end

Here is what the options included in the configuration file do:

  • dev_mode: true/false: If true, bytecode, assets and other files are not copied to the Release folder. Instead, Distillery creates symlinks. This makes creating the Release faster and is great for testing purposes.
  • include_erts: true/false: If true, Distillery includes the Erlang Runtime ERTS. This is not necessary on your development machine but recommended for making self-contained Releases.
    If you want to deploy your Release to a machine with a different operating system or processor architecture, you can specify the path to a cross-compiled version of ERTS instead of true/false.
  • cookie: $STRING: Distillery can set the Erlang cookie. The auto-generated configuration includes random Erlang cookies. Don’t include a hard-coded cookie if you want to commit your configuration to a Git repository! Later in this post we’ll look into a better way to configure the magic cookie.
  • include_src: true/false: This option is available for backwards compatibility with the Erlang Release tool relx. If true, Erlang source code from your project and dependencies is included in the Release. Elixir code is never included, so most of the time, you can ignore this option.
  • version: $VERSION_STRING: Sets the version of the Release. You can use the function current_version/1 to extract the version from your project’s mix.exs.
  • applications: [:app_name]: Here you can list additional applications that you want to include. By default, Distillery includes :runtime_tools which is part of the Erlang standard library and enables certain debugging features.

Lifecycle Scripts aka Hooks

environment :prod do
set pre_start_hook: "rel/hooks/pre_start.sh"
end

Notice that the paths are relative to your project directory and not relative to the rel directory. The most interesting hook is certainly pre_start, but there is also post_start , pre/post_configure , pre/post_stop, and pre/post_upgrade. You can currently only configure one script per hook with Distillery although Erlang would theoretically allow for more.

Umbrella Apps

  1. Include the names of your child applications in the applications list of the Release configuration.
  2. You can choose to take the Release version number from any of the child applications by using current_version(:my_child_app).

As mentioned before, it’s also possible to define more than one Release in your rel/config.exs and thus specify multiple Release profiles. This can be useful if you want to have Releases that only include certain child applications from your umbrella project.

Runtime Configuration With Releases

The Problem with Mix Configuration

If you have anything like foo: System.get_env("BAR_VAR") in a Mix.Config file, you’re out of luck. foo will take on the value of the BAR_VAR environment variable from your build system. When you launch the application on another machine, the value won’t be updated.

Fortunately, there are ways around this. The easiest one is a little trick employed by Distillery:

A Simple Solution: REPLACE_OS_VARS

config :my_app,
foo: "${BAR_VAR}"

This method is great for initializing the Erlang cookie. Use set cookie: "${ERLANG_COOKIE}" in your rel/config.exs to set the cookie from an environment variable.

REPLACE_OS_VARS doesn’t come without problems. Most importantly, this method only works with strings. If you try something like String.to_integer("${BAR}"), you’ll get an error message because the compiler evaluates the expression before Distillery gets a chance to replace the special string.

Fortunately, many libraries now support a more flexible approach to configuration.

Ecto

This callback takes type and the config as arguments. We can safely ignore type (which is either :supervisor or :dry_run, depending on the context of the call). config is the configuration Keyword list from your Mix configuration. This is convenient because it allows you to set default values in your Mix configuration and then override them with environment variables.

The callback has to return {:ok, config} or :ignore. Here is a simple example with type conversion:

Instead of writing your own code for parsing environment variables, you might find a third-party application such as Atmo or Confex useful. I haven’t tried them yet but they seem to have some nice convenience features.

Phoenix

Before you create a production release, make sure your assets are built and digested:

./node_modules/brunch/bin/brunch b -p
MIX_ENV=prod mix phx.digest

With version 1.3, Phoenix has also adapted the Ecto’s callback-style configuration method. This makes it very easy to flexibly configure Phoenix.Endpoint:

The init/2 callback for Phoenix.Endpoint behaves exactly like in Ecto.

Upgrading Releases

You probably know that Erlang and Elixir are compiled to bytecode which is then executed by the BEAM virtual machine. And thanks to BEAM, we can upgrade an application without ever stopping it.

Creating an Upgrade Release

Creating a Production Release

#In Terminal 1
MIX_ENV=prod mix deps.get
MIX_ENV=prod mix release

Now open up a second terminal window and launch the Release:

#In Terminal 2
cd distillery-clock-demo
_build/prod/rel/clock/bin/clock foreground.

Again, you should see the current time printed out every second.

Creating an Upgrade Release

Then, create an upgrade Release by passing the --upgrade flag to mix.release.

#In Terminal 1
MIX_ENV=prod mix release --upgrade

We’ll Do It Live!

#In Terminal 1
_build/prod/rel/clock/bin/clock upgrade 0.2.0

In the second terminal, the Clock output will now change and start to also print out the current date. How neat!

Updating an application wile it’s running!

Are You a Master of Elixir Releases Yet?

If you think I missed an aspect of Elixir Releases that should be part of this (pretty) complete guide, please let me know and I will try to add it!

This article is part of an ongoing series about developing and deploying Elixir applications to production. Upcoming articles will cover deployment strategies and building Docker images.

In this series I am sharing my experience from creating DBLSQD, a release and update server written in Elixir. Check it out, there is a 60-day no-strings-attached free demo:

HackerNoon.com

#BlackLivesMatter