How to make Ecto Repo configuration work in both local and production using environment variables?
0 reputation · 11 Apr 2022, 14:09 UTC
0 reputation · 11 Apr 2022, 14:09 UTC
In a Phoenix project the config/dev.exs file uses config :my_app, MyApp.Repo, url: \"postgres://localhost/my_app_dev\" and the app runs fine locally. In production I switch to a release and change the config to config :my_app, MyApp.Repo, url: System.get_env(\"DATABASE_URL\"). The release starts but the database connection fails, even though DATABASE_URL is set on the host.
It seems the environment variable is not being read when the release boots, or the configuration order is wrong. I also tried setting the variable inside config/prod.exs with System.put_env(\"DATABASE_URL\", \"postgres://...\") but that does not help.
What is the correct pattern for loading environment variables into the Ecto Repo configuration so that it works both locally and when the application is packaged as a release? Are there any pitfalls with Mix.Config precedence or release startup that I should be aware of?
26525 reputation · 11 Apr 2022, 21:02 UTC
The reason your production release fails to read DATABASE_URL is that config/prod.exs is evaluated at compile time. When you package your application as a release, the values from prod.exs are baked into the bytecode. Any call to System.get_env inside that file happens on your build machine, not on your production server.
To load environment variables at startup in a release, you must move production-specific configuration to config/runtime.exs.
config/dev.exs. Since you run this via mix, compile-time configuration is acceptable.
config :my_app, MyApp.Repo,
url: "postgres://localhost/my_app_dev"
config/runtime.exs. This file is executed every time the application starts, regardless of whether it was started via mix or as a release.
import Config
if config_env() == :prod do
config :my_app, MyApp.Repo,
url: System.get_env("DATABASE_URL")
end
System.get_env or System.put_env calls from config/prod.exs to avoid configuration conflicts.
Elixir releases distinguish between compile-time configuration (config.exs, dev.exs, prod.exs) and runtime configuration (runtime.exs).
System.get_env here, the value is fixed at the moment of compilation.To verify the configuration is working without deploying to a full production cluster, you can run your app locally in production mode:
DATABASE_URL=postgres://user:pass@localhost/db_name MIX_ENV=prod mix phx.server
If the application connects successfully, the runtime.exs logic is correct.
Are you using a custom release configuration (e.g., in rel/env.sh.eex) that might be overriding or clearing environment variables before the application boots?
Use comments to ask for clarification. Post a solution as an answer.
26,525 reputation · 11 Apr 2022, 19:06 UTC
One detail worth adding to the runtime.exs approach: prefer System.fetch_env!("DATABASE_URL") over System.get_env/1. With get_env, a missing variable silently stores nil as the URL, and the Repo fails later with a confusing connection error. fetch_env! raises immediately at boot with a clear message naming the missing variable, which is much easier to diagnose in release logs.
Also note that runtime.exs is evaluated in every environment, including dev and test — not just releases. That's why the if config_env() == :prod guard matters; without it, your local mix phx.server would also demand DATABASE_URL and ignore dev.exs.
Finally, if you set pool_size from an env var, remember it arrives as a string: use String.to_integer(System.get_env("POOL_SIZE") || "10"), since Ecto expects an integer.