Runtime.exs and release tasks

I’m using runtime.exs as a way to overwrite config variables (compile time) by environment variables (runtime).
This works.

However, before the application starts: I run _build/prod/rel/production/bin/production eval "MyApp.ReleaseTasks".

defmodule JackJoe.ReleaseTasks do
  alias Ecto.Migrator

  require Logger

  @start_apps [:crypto, :ssl, :myxql, :ecto, :ecto_sql]
  @app :justified

  def migrate_and_seed do
    Logger.info("|> MIGRATE_AND_SEED")

    start_services()
    run_migrate()
    run_seed()
    stop_services()
  end

  def migrate do
    Logger.info("|> MIGRATE")

    start_services()
    run_migrate()
    stop_services()
  end

  def seed do
    Logger.info("|> SEED")

    start_services()
    run_seed()
    stop_services()
  end

  # def rollback(repo, version) do
  #   IO.puts("|> ROLLBACK")
  #   start_services()
  #   {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
  #   stop_services()
  # end

  def run_migrate do
    for repo <- repos() do
      {:ok, _, _} = Migrator.with_repo(repo, &Migrator.run(&1, :up, all: true))
    end
  end

  def run_seed, do: Enum.each(repos(), &run_seeds_for/1)

  #################

  defp start_services do
    Logger.info("|> Starting dependencies...")
    # Start apps necessary for executing migrations
    Enum.each(@start_apps, &Application.ensure_all_started/1)
    Application.load(@app)
    # Start the Repo(s) for app
    Logger.info("|> Starting repos...")
    # Switch pool_size to 2 for ecto > 3.0
    Enum.each(repos(), & &1.start_link(pool_size: 2))
  end

  defp stop_services do
    Logger.info("|> Success!")
    :init.stop()
  end

  defp run_seeds_for(repo) do
    # Run the seed script if it exists
    seed_script = priv_path_for(repo, "seeds.exs")

    if File.exists?(seed_script) do
      Logger.info("|> Running seed script...")
      Code.eval_file(seed_script)
    end
  end

  defp repos, do: Application.get_env(@app, :ecto_repos)

  defp priv_path_for(repo, filename) do
    app = Keyword.get(repo.config, :otp_app)

    repo_underscore =
      repo
      |> Module.split()
      |> List.last()
      |> Macro.underscore()

    Path.join(["#{:code.priv_dir(app)}", repo_underscore, filename])
  end
end

This tasks lacks the overwritten variables from runtime.exs.

import Config

# Settings at runtime for demo apps
IO.inspect("Runtime config")

IO.inspect(System.get_env())
IO.inspect(Application.get_all_env(:my_app))

if Application.get_env(:my_app, :deploy_env) in ["demo"] do
  IO.inspect("Setting runtime config")

  config :my_app, MyApp.Repo, database: System.fetch_env!("DB_NAME")

  config :my_app, MyAppWeb.Endpoint,
    url: [host: System.fetch_env!("HOST"), scheme: "https", port: 443]

  config :my_app,
    cookie_domain: System.fetch_env!("COOKIE_DOMAIN")

  IO.inspect(Application.get_all_env(:my_app))
end

If I inspect the runtime.exs when just starting the application I can clearly see all environment variables and the config from my Application before and after (overwritten).

When running the eval command, the environment variables are printed, but Application.get_all_env(:my_app) results in []. And as a consequence, the migration fails because database is not set.

Well, looks like it loads config/runtime earlier than application controller tab is being loaded (configs are merged and stored in ac_tab application controller tab)

I will not be surprised if apps started with mix behave differently than releases.

My question, though, would be: “can’t you setup deploy_env in runtime.exs, as well?”

I could set deploy_env via environment variables as well.
But if runtime.exs is loaded after config.exs, the freshly set variables would be overwritten the other way.

It is as if in eval commands, the Application module is … not started?

Chippin in on the issue of @pieterm here. We have setup a demo project so anyone can give it a go :slight_smile:

The thing we fail to grasp is why the Application.get_all_env result inside runtime.exs is an empty list? We set a value inside the regular config/prod.exs which should serve as a default, and it will be overwritten inside runtime.exs. So why is it empty?

We assumed the order is:

  • config/prod.exs
    -config/runtime.exs

This is incorrect?

Demo project

1 Like

I suspect it happens because at the point that your runtime.exs is evaluated, your applications are not loaded yet (or maybe eval just doesn’t load them). Application.get_all_env/1 uses :application.get_all_env/1 and the docs for that state:

If the specified application is not loaded […] the function returns [].

Have you checked what the values are when your application is loaded after Application.load(@app)?