Elixir: OTP Applications & Releases

An OTP application is the unit of deployment and boot: a name, a supervision tree, and configuration. Everything you have built so far — processes, GenServers, supervisors — starts and stops together inside one.

The Application Module

# mix new my_app --sup generates exactly this shape.
defmodule MyApp.Application do
  # The application callback: the BEAM calls start/2 when the app boots.
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # A supervisor module from the previous lesson...
      MyApp.Supervisor,
      # ...or an inline child spec for simple cases.
      {MyApp.Cache, ttl: :timer.minutes(5)}
    ]

    # :rest_for_one — if MyApp.Supervisor dies, the Cache restarts too,
    # because it was started after it and may depend on it.
    Supervisor.start_link(children, strategy: :rest_for_one, name: MyApp.Sup)
  end
end

In mix.exs, the application/0 function wires it up: [mod: {MyApp.Application, []}, extra_applications: [:logger]]. From then on mix run --no-halt (dev) or the release script (prod) boots the whole tree.

Application Environment

The app env is a per-application keyword store — the idiomatic place for settings that change per environment. Read it with Application.get_env/3 (or the compile-time Application.compile_env/3).

# config/config.exs — compile-time defaults
import Config
config :my_app, :cache, ttl_minutes: 5

# config/runtime.exs — evaluated at RELEASE boot, so secrets and
# environment-specific values belong here, not in config.exs.
config :my_app, :cache,
  ttl_minutes: String.to_integer(System.get_env("CACHE_TTL_MIN") || "5")

# Anywhere in code:
Application.get_env(:my_app, :cache)[:ttl_minutes]

Distinguish the two moments carefully: config.exs is baked into the release artifact; runtime.exs is read when the node starts. A value used at compile time (a macro, a module attribute) cannot be overridden by a release-time variable — a classic deploy surprise.

Releases for Deployment

mix release assembles the application plus runtime into a directory whose bin/ script is the ops interface. The pieces that matter operationally:

# config/runtime.exs already holds the env-driven settings.
MIX_ENV=prod mix release

# Release environment variables (the important ones):
#   RELEASE_NODE=name@host      distribution name (next lesson)
#   RELEASE_COOKIE=secret       cluster secret — treat as a credential
#   RELEASE_DISTRIBUTION=none|name|full
_build/prod/rel/my_app/bin/my_app daemon
_build/prod/rel/my_app/bin/my_app remote     # shell into the RUNNING node

bin/my_app remote deserves emphasis: it connects an iex session to a live production node, where you can call functions, inspect processes, or hotfix — the same IEx tools, pointed at production. Combine with :observer remotely for a full GUI.

Library Applications

Libraries are applications too, usually without a tree: they ship mod: false (no boot callback) and provide child_specs that the host app mounts in its own supervision tree. That is why adding {Finch, name: MyFinch} to a children list is the standard way to configure a library.

# A library exposing a child spec for hosts to mount:
defmodule MyLibrary do
  def child_spec(opts) do
    %{
      id: {__MODULE__, opts[:name]},
      start: {MyLibrary.Server, :start_link, [opts]},
      type: :worker
    }
  end
end

Practice

  1. Create mix new boots --sup and trace the boot order of the generated tree with Logger lines.
  2. Move one setting from config.exs to runtime.exs, build a release, and verify the value at runtime.
  3. Attach bin/boots remote to a running release and call a function in it.

Next: Distributed Elixir