Keep Elixir secrets in config/runtime.exs, read from the environment or a secrets service when the release boots. Anything in config/config.exs is evaluated at build time, so a secret placed there can be baked into the release you ship and stored in your build artifacts.
What is the difference between build time and runtime config?
The Mix release documentation draws the line clearly. Whenever you run a mix command, Mix loads config/config.exs, and the docs call that build-time configuration because it is evaluated when you compile or assemble a release. Its example shows a System.fetch_env! call inside that file and explains the consequence: the value is read during the build, not when the release starts on the server.
For runtime configuration, the docs tell you to create config/runtime.exs. It runs every time your project or release boots. The documentation shows the pattern directly:
import Config
config :my_app, :secret_key, System.fetch_env!("MY_APP_SECRET_KEY")
Two properties help security. fetch_env! raises if the variable is missing, so a release fails to start instead of running with a blank key. And the same release image can run in staging and production with different secrets.
What rules does runtime.exs have?
The release docs give three. It must import Config at the top. It must not import any other configuration file. And it must not access Mix, since Mix is a build tool and is not available inside releases. The Config module docs add that runtime.exs does not support imports because it is meant to be copied across systems.
There is also a check worth knowing. By default a release compares runtime configuration against any values marked at compile time through Application.compile_env. A mismatch means the system is misconfigured and will not boot. That catches the case where someone moved a secret but a library still read the old compile time value.
What about cookies and secret_key_base?
Plug.Conn documents secret_key_base as a key used to verify and encrypt cookies, and tells you to always use Plug.Crypto.KeyGenerator.generate to derive keys from it rather than using it directly. Cookies can be signed or encrypted. The docs also note that an unsigned and unencrypted cookie value is not automatically escaped.
Treat it like a password: random, long, never committed, and different per environment. Changing it invalidates existing signed and encrypted cookies, so sessions end.
Where should the values come from?
System.fetch_env! only moves the question one step back: who sets the variable? OWASP's Secrets Management Cheat Sheet cautions that environment variables are generally accessible to all processes and may appear in logs or system dumps, and prefers a secrets manager with short lived credentials where possible. Because runtime.exs is plain Elixir code that runs at boot, it can also read a mounted file or call your secrets service. Keep the logic small and avoid printing the values, and keep crash reports from including configuration.
Constructed example
A team has config/prod.exs with a database password from System.get_env. CI builds the release and the image goes to a registry. Anyone who can pull the image can inspect the compiled configuration and find the password. The team moves the call into runtime.exs, changes the database password, and confirms with a test boot that removing the variable makes the release crash with a clear message.
What to do
- Search config.exs, prod.exs and dev.exs for passwords, tokens and keys. Treat finds as exposed.
- Move secret reads into config/runtime.exs and use System.fetch_env!.
- Inject values from your platform or a secrets manager. Avoid baking them into the image.
- Generate a unique secret_key_base per environment and keep it out of git.
- Derive cookie keys with Plug.Crypto.KeyGenerator, never use the base directly.
- Rotate on a schedule and rehearse the session reset.
- Review your Hex dependencies for known issues as part of the same pipeline.
Quick check
Is System.get_env in config.exs safe?
It runs at build time, so the value is captured when you compile or assemble the release. Use runtime.exs for secrets.
What does System.fetch_env! add over get_env?
It raises when the variable is missing, so the app refuses to start without the secret.
Sources
- Mix.Tasks.Release, Elixir documentation
- Config, Elixir documentation
- Plug.Conn documentation
- OWASP Secrets Management Cheat Sheet
Last reviewed: 18 October 2026.