What is this about?

When buliding Elixir applications we often need to run some code on application startup. For example, we may want to run database migrations, attach telemetry handlers, or perform some additional checks.

Is it usually most useful to perform these actions before you start the HTTP Endpoint and the application starts accepting requests. This will allow the deployment system (or load balancer) to check that the application is healthy before sending traffic to it.

How to?

Create a new Startup module in your application:

defmodule MyApp.Startup do
  # Capture compile-time environment
  @compile_env Mix.env()

  def child_spec(_opts) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      # The child process is restarted only if it terminates abnormally
      restart: :transient
    }
  end

  def start_link do
    # We could start a process here, but we don't need to.
    # Instead, we can just run the code we need to run on startup.
    startup(@compile_env)

    # Returning `:ignore` from `start_link/0` callback
    # tells the supervisor that everything is fine and to ignore this child.
    :ignore
  end

  defp startup(:test) do
    # Skip any startup code in test environment
    :ok
  end

  defp startup(_dev_or_prod) do
    # This is just an example. You can run any code here.

    # Migrate on app startup
    Ecto.Migrator.run(MyApp.Repo, :up, all: true)

	  # Perform any other actions
    attach_telemetry_handlers()

    # Run checks against third-party API
    check_api_credentials()
  end

  defp attach_telemetry_handlers do
    # ...
  end

  defp check_api_credentials do
    case MyApp.ImportantApi.ping() do
      {:error, :unauthorized} ->
        # Raise an exception to prevent the application from starting
        raise "Invalid API credentials"
      # ...
    end
  end
end

Then include the Startup module in the list of children in your application’s supervisor children list:

defmodule MyApp.Application do
  def start(_type, _args) do
    children = [
      # ...
      # Add your Startup module to list before the Endpoint
      MyApp.Startup,
      MyAppWeb.Endpoint,
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

References