This is a blog of AmberBit - a Elixir and Ruby web development company. Hire us for your project!

Elixir: Runtime vs. compile time configuration


Posted by Hubert Łępicki

Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.
@hubertlepicki @hubertlepicki

Runtime vs. compile time

Elixir is a compiled language, but when you work with it - it behaves like interpreted language. The whole development workflow of typical Elixir project is very similar to the workflow employed in Ruby projects. You write some code and you run it.

The key difference between Ruby or Elixir is, however, that Elixir does compile the files and persist compiled modules (in *.beam) files on disk. Check out your _build folder to find them.

When you, as a programmer, write Elixir code, you rarely have to explicitly run mix compile. Adding or modifying module will trigger recompilation behind the scenes, when you - for example - start the project with mix phx.server.

Moreover, if you work with Phoenix framework it will re-compile your project between web requests if you change the code, and even push the changes to both - server-side templates and JavaScript code via a WebSocket protocol to client browser - triggering instant update.

It feels very interactive, it feels like interpreted - it feels like Ruby. Or better.

But the compilation phase is still there, and different settings affect different phases. Let’s have a closer look on what is what in typical Elixir & Phoenix project.

Our example app

Let’s generate an example Phoenix 1.3 app for the reference:

$ mix compile_and_runtime
* creating compile_and_runtime/config/config.exs
* creating compile_and_runtime/config/dev.exs
* creating compile_and_runtime/config/prod.exs
You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server


The first thing that Phoenix generator does is in fact creating Mix configuration files.

If you open config/config.exs you should see something like this:

use Mix.Config

config :compile_and_runtime,
  ecto_repos: [CompileAndRuntime.Repo]

config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
  url: [host: "localhost"],
  secret_key_base: "hhtXFfzljiIyKk9fo9/g1R8dJGb/7ZIxEdSfILaxuCB8mwT1g74BWdXru2AAnk8e",
  render_errors: [view: CompileAndRuntimeWeb.ErrorView, accepts: ~w(html json)],
  pubsub: [name: CompileAndRuntime.PubSub,
           adapter: Phoenix.PubSub.PG2]

config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:user_id]

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"

and there will be additional important configuration in your env-specific files imported by the last line. For example, they would configure Ecto / database connection like:

use Mix.Config

# Configure your database
config :compile_and_runtime, CompileAndRuntime.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "postgres",
  password: "postgres",
  database: "compile_and_runtime_dev",
  hostname: "localhost",
  pool_size: 10

Configuration in dev & test mode

When you work on the application on your local computer, you are usually using two Mix environments: :dev and :test. The default one is :dev, so your config/config.exs will load config/dev.exs.

Does this happen at runtime or compile time? Does it matter?

The answer to the first question is: “compile time”. The answer to second is: “probably does not matter”.

During local development, Elixir will load your config/config.exs file, whenever you run a mix task. So, speaking roughly, when you run mix phx.server, the following events will happen:

Phase 1: Initialization

Elixir supports writing Elixir in your configuration files, it also supports executing Elixir code during compilation but it is whole different story to discuss at some other point in time. So the config/config.exs and then config/dev.exs are loaded & executed, and the result is that appropriate Application environment keys will be set.

In this phase you can safely use primitive Elixir types, such as numbers, strings, integers etc. in your configuration files. But you can also write snippets of Elixir.

The most common use case for the later is to provide dynamic configuration, often loaded from shell environment variables.

Let’s say you have this snippet that configures Phoenix Endpoint in config/dev.exs:

config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
    http: [port: 4000],

You can make it depend on environment variable in two ways: either use piece of custom Elixir code that will be executed:

config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
    http: [port: System.get_env("PORT")],

or depend on custom functionality provided by library itself to support dynamic configuration using environment variables. Historically, many Elixir libraries supported this with:

config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
    http: [port: {:system, "PORT"}],

You should not use the above, as not all libraries support this scheme, and it might stop being supported any time in future as well, since there are now better way of doing this. In fact, Phoenix developers already have deprecation of this feature on their to-do list.

For dev and test environments, this way of providing configuration using System.get_env is usually fine. The only gotcha you need to watch out for is that the values in shell environment are always strings. We are relying above again on Phoenix being smart enough to convert strings to integer for us, but we most likely want to write it explicitly anyway:

config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
    http: [port: System.get_env("PORT") |> String.to_integer()],

When this phase finishes, the Application environment is already filled in fully, and the libraries can use Application.get_env/3 to read these values.

Phase 2: Compilation

In this phase, Mix project is being compiled behind the scenes for us. Elixir code we write, or library authors wrote for us, can - and often does - rely on the result of Phase 1. Since we can execute Elixir during compilation of Elixir code, this feature is often used in conjunction with reading Application environment and making some decisions about how and which code to compile.

A good example is Elixir’s Logger, which can be configured to purge all calls with certain level from code on compilation, so they get never executed.

config :logger,
  backends: [:console],
  compile_time_purge_matching: [
    [level_lower_than: :info]

Read more about it in Logger documentation.

More examples can be found libraries that you generally use, one popular example is Ecto, that relies on environment to choose JSON library to use when generating some code common among adapters with bits of meta-programming.

Another example of compile-time configuration, that results in different code being compiled can be found in Poison library:

if Application.get_env(:poison, :native) do
  @compile [:native, {:hipe, [:o3]}]

The snippet above executes at compile-time, and sets @compile module variable that later is used by Erlang compiler directly.

Phase 3: Application start up

After our project’s environment has been populated in Phase 1, and it has been compiled in Phase 2, Mix would execute the compiled code. Here is where the most calls to Application.get_env/3 happen. Phoenix would use it to decide if it should start endpoint or not, Ecto to load it’s configuration etc.

It is often that application environment is only used once, when the supervision tree starts to make decisions on initial configuration.

Phase 4: Execution

It is possible, however, that applications rely on environment configuration during the actual execution, post their start-up phases.

You can use Application.get_env within the functions you write. If you do so, the code will be executed at runtime, Application environment fetched and read at runtime and you can do update the configuration at runtime as well. This comes with slight penalty in terms of performance, but it is used by some libraries out there, for example Crawler uses it to provide default option values.

If you modify application environment during execution with Application.put_env/3, the changes will be visible immediately during next call to the function reading from environment.

Phoenix-specific dev mode note

If you are developing application using Phoenix, it does one more nice thing for you: detects configuration changes in dev mode. If you modify your config files, on the next request you will be presented with an error, saying that you need to re-start the application in order to changes you made to take effect. It’s a neat feature that does not exist if you are writing applications not using Phoenix, so something to watch out for.

Production: Here Be Dragons

For these coming from Ruby, Python or PHP backgrounds, the compiled nature of Elixir starts to show in full form only when they start to do production deployments.

Contrary to interpreted languages, you generally don’t upload the source files to the server. You do not have to have Elixir and Erlang installed on the server either.

Elixir builds upon the concept of Erlang releases, which bundle together compiled application, assets and configuration into single executable, which can be uploaded and executed on the server.

Programmers who were being put off by that procedure, and still can deploy Phoenix applications can use Heroku with custom buildpack and run in production in similar way that you run in dev mode. This means, their application configuration will be loaded, executed before compilation, and they can safely use System.get_env/1 and similar techniques to populate the Application environment on start up.

The releases can be created with several different tools, but distillery has been steadily gaining popularity. It is our default tool to create releases in AmberBit for a while now, and we were delighted by the changes v2.0 brought us.

Traditionally, the problem with releases was that the configuration has been loaded before compilation. And compilation happens somewhere on the build server (or dev’s local machine), but the release itself is being executed on different server.

Since the release has configuration already evaluated and bundled in, one option was to set all of the configuration options on build server. When you upload release to server and run it - all configuration options are then already available in Application environment.

This is not flexible enough, however, in many many cases. You often want to change some configuration option without the need to re-compile and re-upload the whole project to server. This takes time and is tedious task, and surely there are better way.

Distillery prior to version 2.0, provided the mechanism of setting compile-time environment variable (sic!) called REPLACE_OS_VARS to true, and then you had to fiddle around with rel/vm.args file and rel/sys.config file and use special environment variables interpolation syntax in your config/* files to provide runtime configuration behavior, where application would read variables from shell environment.

The syntax in config files looked like:

config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
    http: [port: "${PORT}"],

Good? Not quite.

While the above did work for the most part, the REPLACE_OS_VARS mechanism did only work with strings, and disallowed any arbitrary Elixir code to be executed at runtime. Libraries that accepted strings and converted to integers on their own worked fine, but others did not support it. The following code would not work and crash with error during compilation:

config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
    http: [port: "${PORT}" |> String.to_integer()],

That’s not great, is it?

To work around this issue, many libraries started to provide runtime execution hooks. For example, you could explicitly initialize Phoenix endpoints by adding a hook to endpoint itself.

Problem solved? Not quite. This again has been supported by only some of the libraries, not all. Moreover, it means you have to write a piece of code specifically for a particular environment, and initialization procedures for :dev and :prod would differ. I personally don’t like solutions like that as I find them often causing bugs that are spotted only on production.

Is there a chance for us to have consistent runtime configuration between :dev and :prod or is it a Holly Grail we will never find?

Distillery 2.0

Luckily, Distillery 2.0 solved the problem in rather elegant way. In order to provide a dynamic, runtime configuration, you no longer have to mess around with REPLACE_OS_VARS or Erlang configuration files.

Distillery’s own runtime configuration mechanism allows you to populate parts of Application environment, during release start-up, before any of specified OTP applications are started.

There are several different adapters, which allow you to store this dynamic configuration in different file formats (such as Toml, YAML or JSON). The most convenient one for me in general is - however, Elixir config provider.

Using this provider is straightforward. One has to modify only the rel/config.exs file, to point it to *.exs file containing runtime configuration. This file can be included in the release itself, but it does not have to. You can place your runtime_config.exs file somewhere on the server, and point to it in your release file using absolute path:

set config_providers: [
  {Mix.Releases.Config.Providers.Elixir, ["/etc/app_runtime_config.exs"]}

When you upload a release compiled with the above to the server and execute it, runtime config will be read from /etc/app_runtime_config.exs file, and Application environment populated accordingly before application start up.

In such scenario you might not care about storing or reading environment variables from shell at all, and write values directly to your configuration file.

More common (I think) will be, however, use where you keep your release runtime configuration in repository, and upload it to server along with release itself. In such case crate the runtime configuration in rel/runtime_configuration.exs, and reference from release build config:

release :compile_and_runtime do

  set config_providers: [
    {Mix.Releases.Config.Providers.Elixir, ["${RELEASE_ROOT_DIR}/etc/runtime_config.exs"]}

  set overlays: [
    {:copy, "rel/runtime_config.exs", "etc/runtime_config.exs"}

  set applications: [
    compile_and_runtime: :permanent

This ensures that the rel/runtime_config.exs file committed to repository and present during compilation of release will be copied over to etc/runtime_config.exs relative to where the release is going to be unpacked on the server. Then it loads the file before applications start up.

The runtime_config.exs file syntax itself? It mimics any other Mix.Config file such as config/config.exs or config/dev.exs.

In order to provide dynamic configuration for your :prod environment, move some lines from config/prod.exs to this new rel/runtime_config.exs file and adjust accordingly. The result could be something like:

use Mix.Config
config :compile_and_runtime, CompileAndRuntime.Endpoint,
    http: [port: System.get_env("PORT") |> String.to_integer()],
    url: [host: System.get_env("DOMAIN"), scheme: "https"],
    secret_key_base: System.get_env("SECRET_KEY_BASE")

This way you clearly separate compilation time configuration (and leave it in config/prod.exs) from runtime configuration (and move it to rel/runtime_config.exs.

It is the Holly Grail, and you should update to Distillery 2.0 now :D.


Hi there!

I hope you enjoyed the blog post. Can we help you with Elixir or Ruby work? We are looking for new opportunities at the very moment, and we do have team available just for you.

Email me at: or use the contact form below.

Want to get in touch about a project? Drop us a line!

When submitting the form, you are sending your personal information (including your name and e-mail as entered above) to AmberBit Sp. z o. o. is the receiving party, and a data controller, and will use the information you provided for the purpose of establishing relationship leading to possibly signing a services contract, and fulfillment of such contract only. We will not subscribe you to marketing lists, newsletters etc. You can read more about it in our Privacy Policy.