Elixir: Mix, Dependencies & Releases

Mix is the front door to every Elixir project: it scaffolds apps, runs tests and tasks, manages Hex dependencies, and builds self-contained releases for deployment. This lesson walks a real project from mix new to a production artifact.

Project Anatomy

# mix.exs — the single source of truth for the project.
defmodule Explorer.MixProject do
  use Mix.Project

  def project do
    [
      app: :explorer,
      version: "0.1.0",
      elixir: "~> 1.20",              # minimum Elixir version
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # `:extra_applications` lists OTP apps your app starts with.
  def application do
    [extra_applications: [:logger]]
  end

  defp deps do
    [
      # {package, requirement, options}
      {:req, "~> 0.5"},                        # runtime dep
      {:credo, "~> 1.7", only: [:dev, :test]}  # dev-only tool
    ]
  end
end

Key commands: mix deps.get (fetch), mix deps.update req (bump), mix deps.tree (dependency graph), and mix xref graph (compile-time dependencies inside your own code).

Config Layers

Configuration has two distinct moments, and Elixir 1.9+ made the split explicit. Compile-time config lives in config/config.exs; runtime config lives in config/runtime.exs and is evaluated when the release boots — that is where secrets and environment-driven values belong.

# config/runtime.exs — evaluated at boot, inside the release
import Config

config :explorer,
  # System.fetch_env! CRASHES if the variable is missing — the safe
  # choice for secrets you cannot start without.
  api_key: System.fetch_env!("EXPLORER_API_KEY"),
  # get_env/2 provides a default for optional values.
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

Releases

mix release packages the compiled application plus the BEAM runtime itself into a self-contained directory. The target machine needs nothing installed — no Elixir, no Erlang, no asdf. This is the standard deployment unit.

MIX_ENV=prod mix release          # build into _build/prod/rel/explorer
MIX_ENV=prod mix release --path /opt/explorer

# Operate the release (the script is the whole ops interface):
_build/prod/rel/explorer/bin/explorer start      # run in the foreground
_build/prod/rel/explorer/bin/explorer daemon     # background
_build/prod/rel/explorer/bin/explorer remote     # attach a shell to a LIVE node
_build/prod/rel/explorer/bin/explorer rpc "Explorer.health()"
Pipeline from mix deps.get through compile, config, and release assembly to a self-contained deployment artifact
Fig. 1 — The release bundles compiled code, configuration, and the runtime: deploy the directory, not a language install.

Mix Tasks and Scripts

Two other delivery shapes matter. Mix.install/2 turns a single .exs file into a self-contained program with Hex dependencies — the mechanism the demos in this track use. Custom mix tasks package project tooling for teams.

# A single-file script with dependencies — no project needed:
Mix.install([{:req, "~> 0.5"}])

%{status: 200, body: body} = Req.get!("https://hex.pm/api/packages/ecto")
IO.puts("Ecto docs size: #{byte_size(body)} bytes")

# A custom mix task lives in lib/mix/tasks/hello.ex:
defmodule Mix.Tasks.Hello do
  use Mix.Task
  @shortdoc "Greets the caller"
  @impl Mix.Task
  def run(args), do: IO.puts("Hello, #{args || "world"}!")
end
# invoked as: mix hello Sage

Umbrella Projects

An umbrella project holds several apps in one repository, each with its own mix.exs and dependency set — a middle ground between a monolith and a microservice split. Apps can depend on each other and build in parallel. Choose it when deployment stays unified but ownership boundaries are real; see Designing Elixir Systems for the architecture trade-offs.

mix new store --umbrella
cd store/apps
mix new cart
mix new billing
cd .. && mix test        # runs tests across ALL child apps

Practice

  1. Create a project, add req, and fetch a package's metadata from the Hex API in iex -S mix.
  2. Add runtime.exs config that fails fast when a variable is missing; observe the boot error.
  3. Build a release and run bin/ rpc to call a function in the live node.

Next: Testing with ExUnit