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
end
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
end
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.