What is this about?
The default configuration files for new Phoenix projects looks like this:
config
├── config.exs
├── dev.exs
├── prod.exs
├── runtime.exs
└── test.exs
While this is all good there is still room for improvement especially for local development or testing.
How to?
First of all, while we should never put production secrets in our code repository, after years of working with different approaches I’ve come to the conclusion that it’s fine to put development secrets directly in the config/dev.exs
. It’s just so much easier to have them there, instantly available for the whole team without any additional setup step - much better than copy & pasting secrets from password manager or Slack. This appraoch requires that development secrets are low-risk and do not pose any real danger when leaked which should always be the case anyway.
Even with all secrets in commited config/dev.exs
file we still sometimes need to change some config values locally. For example, we may want to use different database credentials, change logger settings or disable Oban pluginsl. We could do this by editing the config/dev.exs
file but that opens up the possibility of accidental commit of local changes to the repository.
Instead, we can create a separate config/dev.local.exs
file and put all local config there. This file is not commited to the repo so we can safely change it without worrying about commiting it by accident.
All you need to do is to add this line at the bottom of config/dev.exs
file:
# config/dev.exs
if File.exists?("config/dev.local.exs"), do: import_config("dev.local.exs")
and this line to .gitignore
file to prevent local config files from being committed:
# Ignore local config files
/config/*.local.exs
Then create config/dev.local.exs
file and add your local config there.
This approach works analogically for test environment with config/test.local.exs
etc.
What to put in local config?
You can put anything you want and find useful. Here are some real world examples:
# config/dev.local.exs
import Config
# Change logger level
config :logger, level: :warning
# Configure database credentials
config :myapp, Myapp.Repo,
username: "postgres",
password: "postgres"
# Configure other secret values
config :myapp, SomeThirdParty,
oauth_client_id: "xxx",
oauth_client_secret: "xxx",
# Configure local URL
config :myapp, MyAppWeb.Endpoint,
url: "https://some-tunneling-service"
# Enable/Disable ecto logger
config :myapp, MyApp.Repo, log: false
# Enable/Disable tesla debug logger
config :tesla, Tesla.Middleware.Logger, debug: true
# Enable/Disable certain Ev handlers
config :myapp, MyApp.Events,
handlers: [
MyApp.Notifications,
# MyApp.Mixpanel
]
# Enable/Disable certain Oban queues/plugins
config :myapp, Oban,
plugins: [
# Disable Pruner & Cron
# ...
],
queues: [
# Disable certain queue
some_queue: 0
]
Config files caveats
Nested lists are always merged
There are few non-obvious things to keep in mind when working with multiple config files. You need to keep in mind that the config values are always merged.
See the following example:
# config/config.exs
config :hello, Oban,
queues: [
a: 1,
b: 2
]
# config/dev.exs
config :hello, Oban,
queues: [
# keep only one queue
a: 3
]
While we wanted to keep only one Oban queue, the resulting config still has all the queues:
iex(1)> Application.get_env(:hello, Oban)
[queues: [b: 2, a: 3]]
Instead, we need to clear the value first and then set it again:
# config/dev.exs
# clear the value first
config :hello, Oban, queues: nil
# then set it again
config :hello, Oban,
queues: [
a: 1
]
Now the resulting config has only one queue as intended:
iex(1)> Application.get_env(:hello, Oban)
[queues: [a: 1]]
Removing certain values from lists
There are situations where instead of overriding a nested list, you want to remove certain values from it. For example, you may want to remove only one plugin from Oban’s list of plugins or only one Ev handler.
To prevent copying the whole list from main config and then keeping it in sync when it changes we can read the main config and make adjustments to it.
I definitely don’t recommend this approach for config files commited to the repo as it’s complex and not always obvious, but it’s fine for your local config.
# config/dev.local.exs
# Read current config
defaults = Process.get({Config, :config})[:myapp]
# Remove only Cron plugin:
# clear the value first
config :myapp, Oban,
plugins: nil
# set to defaults minus Cron plugin
config :myapp,
plugins: List.keydelete(defaults[Oban][:plugins], Oban.Plugins.Cron, 0)
# Remove only GooglePubSub event handler:
# clear the value first
config :myapp, MyApp.Events,
handlers: nil
# set to defaults minus GooglePubSub
config :myapp, MyApp.Events,
handlers: defaults[MyApp.Events][:handlers] -- [MyApp.GooglePubSub]