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.
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:
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:
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 about these the Config module and build vs runtime configuration:
Finally, we need to import the secret file from dev.exs.
Add this to the bottom:
Where are the env variables?
By using the recommended 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:
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. It is common for the config/config.exs file itself import other 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.
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
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)
load_from_system_env: true will tell Repo and Endpoint that we want to load System variables.
Step 2: Add init function to Repo
Open LIB/repo.ex and add the init callback: (don't forget to change your App)
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 false. (development etc)
Step 3: Add init to Endpoint
Open WEB/endpoint.ex and add:
Same thing here, except we're configuring http and url using a list.
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!