When you use Elixir releases to deploy your application, you may find yourself in a situation where not all of the compile-time dependencies are included in the release.

The problem is particularly annoying, since you likely to have a CI pipeline building the project and testing it successfully, then it’s being built and deployed to server. You will only learn about missing runtime dependency that is within release at the point when you attempt to start, or even after you started your release on the server. This hopefully happens on your staging and not production server, but not everyone has set up like that.

If things compile nicely, work on your dev machine, yet you find yourself having module X is unavailable when starting release you may have the very same issue I encountered.

To check that this is happening, build the release locally with mix release. Then, check _build/dev/rel/your_app/lib/ folder to verify that one of the used libraries, defining module X is indeed missing from the release.

The fix: Just add the missing dependency to your own mix.exs?

The first “fix” that is going to work is to add the dependency to your project’s mix.exs file. If you have not been messing around with :applications vs. :extra_applivations vs. :included_applications, adding the missing library to the deps section is going to be enough:

# mix.exs
def deps do
  {:missing_lib_x, "~> 0.0.1"} # <- add your missing library here

Rebuild the release with mix release and see if that fixed the problem.

If not, it may be the sign that you messed around with :applications, and you either want to change what you do there or include the missing dependency into “project” section of mix.exs like is described in official documentation.

The above fixes will work but what on earth has caused the issue?

The story gets interesting, because you would expect all of your dependencies, and dependencies of your libraries to be included in a release. If it’s in mix.lock it should be in the release. Right? Well, not quite.

Elixir OTP applications have two separate set of dependencies: compile-time dependencies and run-time dependencies.

Compile-time dependencies are what you declare in the “deps” function in your mix.exs. When building a project, Elixir (Mix) figures out the dependency tree and fetches and compiles all dependencies of your app - and their direct or indirect dependencies likewise. This is what ends up in mix.lock file.

You declare run-time dependencies in your mix.exs file as well, but you generally use :extra_applications. I have written in details about it in this blog post from 2007 when :extra_applications was still fairly fresh addition to Elixir. If you follow that pattern, by default all of your compile-time dependencies become your run-time dependencies. You can then selectively make some of them only a compile-time dependency by adding runtime: false in their entry in “deps” like this:

# mix.exs
def deps do
  {:compile_time_only_lib_x, "~> 0.0.1", runtime: false} # <- won't be started at application boot

This is a good pattern and in ideal world all library authors would use it too, but we are not living in ideal world :D.

All declared run-time dependencies will be started when your OTP application starts, and all of their direct or indirect run-time dependencies will be started as well.

The same run-time dependency tree is also used by Mix.Release when figuring out which applications to include in final release. The relevant fragment of code that does the job can be found on GitHub.

OK, but why has it happened?

You, or - more likely - one of the libraries you use has messed up their run-time dependencies, and decided not to include a library it depends at runtime on. Likely, you - or they - used :applications and not :extra_applications, to specify their run-time dependencies.

If it was your mistake, just add the missing library to run-time dependencies. If you noticed the issue on the library you just included, that’s probably a bug in the library, and as a good member of the community you can either report the issue or provide a fix yourself.

Library authors may not know about the above mix release behavior, and technically they often don’t have to start given dependency at runtime, just load it. Not all Elixir libraries come with their own supervision trees, so you may not need to actually start them with anything like Application.start at all. It just needs to be there, for the mix release to correctly pick it up and include in release. Unfortunately, these libraries, even that they don’t have to be started, have to be explicitly declared as run-time dependencies by your application or one of it’s dependencies to be included in the release.

Hope that helped!

Post by Hubert Łępicki

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