Environment variables are commonly used to connect different services and configure an application for different environments. Configuration is handled differently in Elixir and Phoenix applications but there are useful patterns to get started with and improve on as your system requires.
We’re going to cover a handling development and production configuration for Phoenix applications for Releases introduced in Elixir 1.9, and for production deployment using tools like Distillery.
We’ll configure development using a dev.secret.exs file, and also cover build-time vs run-time configuration in different contexts.
Development Environment
We’ll start with the development environment.
Create Development File
Create config/dev.secret.exs
in your Phoenix Application config directory.
Before we add sensitive data let’s add it to the .gitignore file so it won’t be stored in the git repository:
# .gitignore
...
**/*.secret.exs
This will keep sensitive data safe.
Add Sensitive Configuration
Add any sensitive configuration to dev.secret.exs,
such as a remote DB url connection or external service api key:
import Config
config :my_app, MyApp.Repo,
url: "ecto://user:[email protected]:5432/app_dev"
config :external_service,
api_key: "123123"
You’ll notice the we’re importing the Config module which is a simple keyword-based configuration API.
This configuration is evaluated at build time which works for our development environment as we recompile changes quickly as needed with a restart.
NOTE: It’s helpful to read the docs for the Config module, and build vs runtime configuration.
- Config Module
- Mix Release - build-time configuration
- Mix Release - runtime configuration
Import dev.secret.exs
Finally, we need to import the secret file from dev.exs.
Add this to the bottom:
import_config "dev.secret.exs"
Where are the env variables?
By using the Config
module we have eliminated the need to use local environment variables.
We can safely hard-code our configuration in dev.secret.exs, exclude it from the repository and it’s evaluated at build time while the application compiles for local development.
Accessing Configuration Vars
Depending on the service package, this will mostly likely be handled internally.
To access the variables set in config files:
Application.fetch_env!(:external_service, :api_key)
Production Configuration (releases)
The release system in Elixir 1.9 is an important improvement for Elixir applications that provides ways to set configuration at run-time.
We should cover the differences between build vs run time configuration in more detail as it’s important to understand before using releases.
The config/prod.exs
file is evaluated at build-time according to the docs:
config/config.exs
(and config/prod.exs
) - provides build-time application configuration, which are executed when the release is assembled.
Whenever you invoke a mix command, Mix loads the configuration in config/config.exs,
if said file exists.
The config/config.exs
file will import environment specific configuration, based on the current MIX_ENV
, such as config/dev.exs
, config/test.exs
, and config/prod.exs
.
We say that this configuration is a build-time configuration as it is evaluated whenever you compile your code or whenever you assemble the release.
Configuring our application for production in these files may not work if there are separate build/target environments and other nuances. Additionally, we need to keep sensitive data safe.
Runtime Configuration
To enable runtime configuration the recommended pattern is to create config/releases.exs
which will be copied to your release and executed as soon the system starts.
In a default Phoenix installation there is a prod.secret.exs
file which can renamed to releases.exs
# config/releases.exs
import Config # Config module
config :phxroad, Phxroad.Repo,
url: System.fetch_env!("DATABASE_URL"),
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
config :phxroad, PhxroadWeb.Endpoint,
secret_key_base: System.fetch_env!("SECRET_KEY_BASE"),
server: true
There are a few System module functions being used:
-
System.get_env("POOL_SIZE") || "10")
- tries to find the variable on the Host or sets a default. -
System.fetch_env!("DATABASE_URL")
- will raise an error if not found on the Host.
With this setup, environment variables set on the target Host will be evaluated at runtime while the application starts.
Production Config (Distillery / CI)
If you’re using something like Distillery to build releases there is another helpful pattern for dynamic configuration which involves using an init
/2 function.
Using this function we can configure the Phoenix application at runtime using System environment variables.
We’re going to configure the production DATBASE_URL
, SECRET_KEY_BASE
and PORT
, but this pattern can also be used for CI builds and tests.
Step 1: Move configs out of exs
Open config/prod.exs
and replace dynamic configs: (don’t forget to change your App)
import Config
config :phxroad, Phxroad.Repo,
load_from_system_env: true, # <--- added
# url: <--- moved to Repo.init
pool_size: 20
config :phxroad, PhxroadWeb.Endpoint,
load_from_system_env: true, # <--- added
# http: <--- moved to Endpoint.init
# url: <--- moved to Endpoint.init
# secret_key_base: <--- moved to Endpoint.init
cache_static_manifest: "priv/static/cache_manifest.json",
server: true,
code_reloader: false
config :logger, level: :info
load_from_system_env
: true will tell Repo and Endpoint that we want to load System variables.
Step 2: Add init function to Repo
Repo docs: https://hexdocs.pm/ecto/Ecto.Repo.html#c:init/2
Open LIB/repo.ex
and add the init callback: (don’t forget to change your App)
defmodule MyApp.Repo do
...
def init(_type, config) do
if config[:load_from_system_env] do
db_url =
System.get_env("DATABASE_URL") ||
raise("expected the DATABASE_URL environment variable to be set")
config = Keyword.put(config, :url, db_url)
{:ok, config}
else
{:ok, config}
end
end
end
Here’s what’s happening:
-
checks for
load_from_system_env
true -
attempts to configure
:url
for from system vars - raises an error if the variable is not found
-
loads configs normally if
load_from_system_env
is false. (dev etc)
Step 3: Add init to Endpoint
Docs: https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#c:init/2
Open WEB/endpoint.ex
and add:
defmodule PhxroadWeb.Endpoint do
...
def init(_key, config) do
if config[:load_from_system_env] do
port =
System.get_env("PORT") ||
raise("expected the PORT environment variable to be set")
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise("expected the SECRET_KEY_BASE environment variable to be set")
config =
config
|> Keyword.put(:http, [:inet6, port: port])
|> Keyword.put(:secret_key_base, secret_key_base)
|> Keyword.put(:url, [host: "phxroad.com", port: port])
{:ok, config}
else
{:ok, config}
end
end
...
end
Same thing here, except we’re configuring http and url using a list.
Conclusion
Runtime configuration for Elixir apps is a complex subject but we covered some basic patterns for development, production and CI/Test environments. We also touched on the differences between compile-time vs runtime configuration for Phoenix Applications.
Releases added some important APIs for handling configuration. Understanding how to use these systems in Phoenix applications for different deployment pipelines and target hosts is an important piece of the overall pie.
Thanks for reading and I hope you found this useful!
We’re deep-diving into some deployment strategies next so stay tuned!