environment variables, phoenix

Handling Environment Variables in Phoenix

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.

Versions

  • Elixir 1.9.4
  • Phoenix 1.4.11

Table of Contents

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 about these the Config module and build vs runtime configuration:

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:

$ 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. 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.

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 Phxroad.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 false. (development 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!

Further Reading:

Author image

About Troy Martin

Ruby, Elixir and Javascript Software Developer.