Runtime Configuration
A release is built once and then started on many machines: staging and
production, three regions, a laptop. Each needs its own settings (database
address, port, secrets), and secrets must never be baked into the build. But
config/config.exs is evaluated when the release is built: whatever it
reads, it reads on the build machine, and the value is frozen into the
release.
config/runtime.exs is the answer: it's evaluated every time the release
starts, on the machine it starts on, so it can read that machine's
environment variables.
On this page:
- Both files read
REGION. We build the release withREGION=build-boxand start it withREGION=eu-west:config.exsstill saysbuild-box,runtime.exssayseu-west. runtime.exsrequiresAPI_KEY. Started without it, the release refuses to boot and says why.- A value read with
Application.compile_env/2is frozen into the compiled code. Changing it at runtime is caught at boot.
The project
defmodule Settings.MixProject do
use Mix.Project
def project do
[app: :settings, version: "0.1.0", elixir: "~> 1.20", deps: []]
end
end
import Config
# Evaluated when the release is BUILT, on the build machine.
config :settings, built_in: System.get_env("REGION", "unset")
config :settings, greeting: "hello"
import Config
# Evaluated every time the release STARTS, on the machine it starts on.
config :settings, region: System.get_env("REGION", "unset")
config :settings, api_key: System.fetch_env!("API_KEY")
if greeting = System.get_env("GREETING") do
config :settings, greeting: greeting
end
Settings.report/0 prints what each file set; Settings.greeting/0 returns a
value fixed at compile time.
defmodule Settings do
# Read once, when this module is compiled, and frozen into its code.
@greeting Application.compile_env(:settings, :greeting)
def greeting, do: @greeting
def report do
IO.puts("config.exs said: " <> Application.fetch_env!(:settings, :built_in))
IO.puts("runtime.exs said: " <> Application.fetch_env!(:settings, :region))
end
end
Build on one machine, start on another
The build sees REGION=build-box. Note the line saying the release will use
config/runtime.exs at runtime.
$ REGION=build-box MIX_ENV=prod mix release
Compiling 1 file (.ex)
Generated settings app
* assembling settings-0.1.0 on MIX_ENV=prod
* using config/runtime.exs to configure the release at runtime
Release created at _build/prod/rel/settings
# To start your system
_build/prod/rel/settings/bin/settings start
Once the release is running:
# To connect to it remotely
_build/prod/rel/settings/bin/settings remote
# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/prod/rel/settings/bin/settings stop
To list all commands:
_build/prod/rel/settings/bin/settings
Started with REGION=eu-west, the two files disagree: one value was decided at
build time, the other just now.
$ REGION=eu-west API_KEY=k-123 _build/prod/rel/settings/bin/settings eval 'Settings.report()'
config.exs said: build-box
runtime.exs said: eu-west
A missing secret stops the boot
Without API_KEY, System.fetch_env!/1 raises while the configuration is
being read, and the release stops before any of the app runs. A loud, immediate
failure beats an app that boots and fails on its first request.
$ REGION=eu-west _build/prod/rel/settings/bin/settings eval 'IO.puts(:booted)'
ERROR! Config provider Config.Reader failed with:
** (System.EnvError) could not fetch environment variable "API_KEY" because it is not set
(elixir 1.20.5) lib/system.ex:731: System.fetch_env!/1
_build/prod/rel/settings/releases/0.1.0/runtime.exs:5: (file)
(elixir 1.20.5) src/elixir.erl:382: :elixir.eval_external_handler/3
(stdlib 8.1.1) erl_eval.erl:1048: :erl_eval.do_apply/7
(stdlib 8.1.1) erl_eval.erl:1664: :erl_eval.expr_list/7
(stdlib 8.1.1) erl_eval.erl:1660: :erl_eval.expr_list/5
(stdlib 8.1.1) erl_eval.erl:451: :erl_eval.expr/6
(stdlib 8.1.1) erl_eval.erl:441: :erl_eval.expr/6
Runtime terminating during boot ({#{env=><<"API_KEY">>,'__struct__'=>'Elixir.System.EnvError','__exception__'=>true},[{'Elixir.System','fetch_env!',1,[{file,"lib/system.ex"},{line,731}]},{elixir_compiler,'__FILE__',1,[{file,"_build/prod/rel/settings/releases/0.1.0/runtime.exs"},{line,5}]},{elixir,eval_external_handler,3,[{file,"src/elixir.erl"},{line,382}]},{erl_eval,do_apply,7,[{file,"erl_eval.erl"},{line,1048}]},{erl_eval,expr_list,7,[{file,"erl_eval.erl"},{line,1664}]},{erl_eval,expr_list,5,[{file,"erl_eval.erl"},{line,1660}]},{erl_eval,expr,6,[{file,"erl_eval.erl"},{line,451}]},{erl_eval,expr,6,[{file,"erl_eval.erl"},{line,441}]}]})
Crash dump is being written to: erl_crash.dump...done
Compile-time values can't change at runtime
@greeting was set from compile_env during the build. Setting GREETING
makes runtime.exs change that key, so the running app would disagree with
its own compiled code. The release checks for this at boot and refuses.
$ API_KEY=k-123 GREETING=hi _build/prod/rel/settings/bin/settings eval 'IO.puts(Settings.greeting())'
ERROR! the application :settings has a different value set for key :greeting during runtime compared to compile time. Since this application environment entry was marked as compile time, this difference can lead to different behavior than expected:
* Compile time value was set to: "hello"
* Runtime value was set to: "hi"
To fix this error, you might:
* Make the runtime value match the compile time one
* Recompile your project. If the misconfigured application is a dependency, you may need to run "mix deps.clean settings --build"
* Alternatively, you can disable this check. If you are using releases, you can set :validate_compile_env to false in your release configuration. If you are using Mix to start your system, you can pass the --no-validate-compile-env flag
Runtime terminating during boot ({<<"aborting boot">>,[{'Elixir.Config.Provider',boot,2,[]}]})
Crash dump is being written to: erl_crash.dump...done
Pitfall: anything
config.exs(orprod.exs) reads from the environment comes from the build machine. Read environment variables only inruntime.exs, and keep secrets out of the files evaluated at build time.
Pitfall: use
Application.compile_env/2only for values that really must be known at compile time (they change the generated code). For everything else callApplication.fetch_env!/2when you need the value.
Pitfall: environment variables are inherited by every program your release starts and are easy to leak through logs or debug output. For real secrets, mount files from your platform's secret store and read those in
runtime.exs.