After you have spent time to develop your Elixir application, you have the challenge of deploying it. I call it a challenge because, without taking into account complex scenarios (scalability, reliability and so on), you will be pretty soon bumping into some unexpected behaviours; this happens because of the difference between build time and run time environment.

Build time vs run time

During development you run your application by launching

$ mix phx.server

and probably you have a set of exported environment variables that are used by your application, in config|dev|..|.exs file, like this:

config :app, payments_key: System.get_env("PAYMENTS_KEY")

which could be the key of some remote payment service your app is using. As long as you are using mix phx.server you are de facto running a mix application, and this solution will work.

If you, instead, create a production build and run it, properly exporting environment variables would not just work.

This happens because the System.get_env(..) instruction is evaluated at compile time, and so, providing the values after such step would cause an empty value to be found instead of the expected one.

Imagine we have a controller that loads the values of a given application key:

{% gist 3737b82a9a3addc88f23c84e747c7e23 %}

served at /api/keys ; by adding the following configuration in config.exs (so in the base file, extended by [dev|test|prod].exs files) and running the app you'll see:

$ PAYMENT_KEY=pk mix phx.server Compiling 11 files (.ex) Generated app app [info] Running ElixirTestOneWeb.Endpoint with cowboy 2.6.3 at 0.0.0.0:8080 (http) [info] Access ElixirTestOneWeb.Endpoint at http://localhost:8080

$ curl http://localhost:8080/api/keys | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 29 100 29 0 0 475 0 --:--:-- --:--:-- --:--:-- 483 { "keys": { "payment_key": "pk" } }

And it will work for all the other envs (try running MIX_ENV=prod PAYMENT_KEY=pk mix phx.server and verify).

Release creation

Let's try what happens if we create a release build:

# Needed to generate the release configuration file $ MIX_ENV=prod mix release.init

An example config file has been placed in rel/config.exs, review it, make edits as needed/desired, and then run `mix release` to build the release

$ MIX_ENV=prod mix release --env=prod ==> Assembling release.. ==> Building release app:0.1.0 using environment prod ==> Including ERTS 10.3.1 from /usr/local/Cellar/erlang/21.3.2/lib/erlang/erts-10.3.1 ==> Packaging release.. Release successfully built! To start the release you have built, you can use one of the following tasks:

# start a shell, like 'iex -S mix' > _build/prod/rel/app/bin/app console

# start in the foreground, like 'mix run --no-halt' > _build/prod/rel/app/bin/app foreground

# start in the background, must be stopped with the 'stop' command > _build/prod/rel/app/bin/app start

If you started a release elsewhere, and wish to connect to it:

# connects a local shell to the running node > _build/prod/rel/app/bin/app remote_console

# connects directly to the running node's console > _build/prod/rel/app/bin/app attach

For a complete listing of commands and their use:

> _build/prod/rel/app/bin/app help

and run it as suggested by the resulting output:

$ PAYMENT_KEY=pk HOSTNAME=localhost PORT=8080 _build/prod/rel/app/bin/app foreground

Trying to hit the endpoint for the values will generate the following result:

$ curl http://localhost:8080/api/keys | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 29 100 29 0 0 4443 0 --:--:-- --:--:-- --:--:-- 4833 { "keys": { "payment_key": null } }

This happens because res = Application.get_env(:app, :config_keys) is trying to access a value that has been evaluated at build time, while we provided (correctly) at run time.

Distillery config providers

The proper way to handle this is by using Distillery Config Providers: long story short, it's a way to inject configuration that will be evaluated at run time, so that System.get_env commands will be correctly valued.

It's a very easy solution, because all it takes is to prepare a proper configuration file to be used during the release build.

The first step is to create the release file with MIX_ENV=prod mix release.init . This will create a file config.exs that contains information about environments and releases (info). In order to inject the proper configuration provider, you need to add, in the prod environment, the following config lines:

set( config_providers: [ {Mix.Releases.Config.Providers.Elixir, ["${RELEASE_ROOT_DIR}/config/runtime.exs"]} ] )

set( overlays: [ {:copy, "config/runtime.exs", "config/runtime.exs"} ] )

This tells the system to make two things: to copy config/prod.exs as ${RELEASE_ROOT_DIR}/config/runtime.exs and use it as configuration file.

So, if we now build the app, everything will work fine:

$ ~/dev/elixir/gcp_article/app(master*) » MIX_ENV=prod mix release --env=prod ==> Assembling release.. ==> Building release app:0.1.0 using environment prod ==> Including ERTS 10.3.1 from /usr/local/Cellar/erlang/21.3.2/lib/erlang/erts-10.3.1 ==> Packaging release.. Release successfully built! To start the release you have built, you can use one of the following tasks:

# start a shell, like 'iex -S mix' > _build/prod/rel/app/bin/app console

# start in the foreground, like 'mix run --no-halt' > _build/prod/rel/app/bin/app foreground

# start in the background, must be stopped with the 'stop' command > _build/prod/rel/app/bin/app start

If you started a release elsewhere, and wish to connect to it:

# connects a local shell to the running node > _build/prod/rel/app/bin/app remote_console

# connects directly to the running node's console > _build/prod/rel/app/bin/app attach

For a complete listing of commands and their use:

> _build/prod/rel/app/bin/app help

$ ~/dev/elixir/gcp_article/app(master*) » PAYMENT_KEY=pk HOSTNAME=localhost PORT=8080 _build/prod/rel/app/bin/app foreground 13:36:53.482 [info] Running ElixirTestOneWeb.Endpoint with cowboy 2.6.3 at 0.0.0.0:8080 (http) 13:36:53.482 [info] Access ElixirTestOneWeb.Endpoint at http://8080:8080

$ » curl http://localhost:8080/api/keys | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 29 100 29 0 0 4583 0 --:--:-- --:--:-- --:--:-- 4833 { "keys": { "payment_key": "pk" } }

At this point, you can orchestrate your deployment making proper configuration provisioning.

The future

Version 1.9 of Mix.Config will contain the release command, so to import the release features now offered by distillery and much more. Read about it here.

Note: the code for this article is published here.

Photo by John Barkiple on Unsplash