Switch to a config file per app instead of a config file per environment

Hi,

There are a few writeups describing alternative config arrangements by a topic (or an OTP app):

I tried it out on a hobby project and found it more convenient than a configuration per environment, and therefore, I’d like to propose changing the default.

In my case, the whole of config.exs was just five lines long

import Config

for config_file <- Path.wildcard(Path.join([File.cwd!(), "config", "compile_time", "*.exs"])) do
  import_config(config_file)
end

The longest file in the compile_time dir, the endpoint configuration was

import Config

config :my_app, MyAppWeb.Endpoint,
  url: [host: "localhost"],
  render_errors: [
    formats: [html: MyAppWeb.ErrorHTML, json: MyAppWeb.ErrorJSON],
    layout: false
  ],
  pubsub_server: MyApp.PubSub,
  live_view: [signing_salt: "tU71HhHQ"]

case config_env() do
  :dev ->
    config :my_app, MyAppWeb.Endpoint,
      http: [ip: {127, 0, 0, 1}, port: 4001],
      check_origin: false,
      code_reloader: true,
      debug_errors: true,
      secret_key_base: "ztwTR1eV8qJmsKqFsAHcSu49my17XV5KULr6okIBXz9aRJEc4t8L2q3okO1L+ujE",
      watchers: [
        esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
        tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
      ]

    config :my_app, MyAppWeb.Endpoint,
      live_reload: [
        patterns: [
          ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
          ~r"priv/gettext/.*(po)$",
          ~r"lib/my_app_web/(controllers|live|components)/.*(ex|heex)$"
        ]
      ]

  :test ->
    config :my_app, MyAppWeb.Endpoint,
      http: [ip: {127, 0, 0, 1}, port: 4002],
      secret_key_base: "oKSvXrmhGIz0TZUqB5PzlzJREnh7I1+TMCyWlvEhCR/AgN3jec4kl2s+qTkZsqTP",
      server: false

  _ ->
    :ok
end

I also split runtime.exs under config/runtime, which required a bit more ceremony:

  • figure out where from configs should be read (source vs release
  • copying runtime configs during release assembly.
# config/runtime.exs
import Config

topic_configs_path =
  if config_env() == :prod do
    root_dir = :code.priv_dir(:my_app)
    release_version = Application.spec(:my_app, :vsn) |> to_string()

    runtime_path =
      [root_dir, "..", "..", "..", "releases", release_version, "runtime"]
      |> Path.join()
      |> Path.expand()

    Path.join([runtime_path, "*.exs"])
  else
    Path.join([File.cwd!(), "config", "runtime", "*.exs"])
  end

for config_file <- Path.wildcard(topic_configs_path) do
  Code.require_file(config_file)
end
defmodule MyApp.MixProject do
  def project do
    [
      releases: [
        sesame: [
          steps: [:assemble, &copy_prod_runtime_configs/1],
        ]
      ]
    ]
  end
 defp copy_prod_runtime_configs(%Mix.Release{version_path: path} = release) do
    release_config_dir = "runtime"
    File.mkdir!(Path.join([path, release_config_dir]))

    all_configs =
      [File.cwd!(), "config", "runtime", "*.exs"]
      |> Path.join()
      |> Path.wildcard()

    for config_file <- all_configs do
      dst_path = Path.join([path, release_config_dir, Path.basename(config_file)])
      Logger.info("COPYING #{config_file} as #{dst_path} ")

      File.cp!(
        config_file,
        dst_path
      )
    end

    release
  end

Btw, if anyone have suggestions on better ways to get a releases directory than

root_dir = :code.priv_dir(:my_app)
release_version = Application.spec(:my_app, :vsn) |> to_string()

runtime_path =
      [root_dir, "..", "..", "..", "releases", release_version, "runtime"]
      |> Path.join()
      |> Path.expand()

that would be a very welcomed change

1 Like

The question is, how do you plan on enforcing correct app configuration, do you make any checks whether the correct application is configured in those .exs files?

How do you mean?

I‘m not a fan tbh. I don‘t want to look through N files just to figure out how my project is configured for e.g. testing or development. There might even be M files out of the N, which don‘t even have any config for a specific env. But I‘d still need to look at those just to notice that.

2 Likes

What’s the use case for looking into how a “project” is configured vs. peeking into a configuration for a specific app, an ecto repo or a phx endpoint?

I‘ve worked on a project with quite a few dependencies and nerves (multiple MIX_TARGETs) involved. Knowing how things are configured for a certain place code runs in is more important than how individual parts might be configured. Often configuration on app A kinda correlates with configuration on app B for a certain target and you loose that relation quite easily if it is scattered around a handful of individual config files, mixed in an even larger set of existing config files.

It would be a pain to build a mental model of how target A is configured over target B if you need to look through 10-30 individual files, parse all their conditionals and combine in your mind how all the individual pieces are configured to make the larger whole work. I prefer have less branches and larger chunks of „all this happens together in this branch“ over branching by individual applications and then branching those individually by all already existing criteria.

1 Like

That project sounds like an outlier to me.

If you have already made up your mind then use your way, nobody is stopping you. No need to argue which case is more representative, plus you are biased in this case so it’s not an honest discussion.

If you really want to argue statistics and representation: I worked on no less than 25 Elixir projects and all of them used per-env config files, and bigger teams also employed sorting configs by app name alphabetically which made them trivial to browse and modify.