Handling Environment Variables in Ruby

Configuring your Rails application can be tricky. How do you define secrets? How do you set different values for your local development and for production? How can you make it more maintainable and easy to use?

Using environment variables to store information in the environment itself is one of the most used techniques to address some of these issues. However, if not done properly, the developer experience can deteriorate over time, making it difficult to onboard new team members. Security vulnerabilities can even be introduced if secrets are not handled with care.

In this article, we’ll talk about a few tools that we like to use at OmbuLabs and ideas to help you manage your environment variables efficiently.

12 Factor Apps

In 2011, Heroku opens a new window created The Twelve-Factor App opens a new window methodology aimed at providing good practices to simplify the development and deployment of web applications.

As the name suggest, the methodology includes twelve factors, and the third factor states that the configuration of the application should be stored in the environment.

The idea of storing configuration in the environment was not created by Heroku but the popularity of Heroku for Ruby and Rails applications made this approach really popular.

The main benefit is that our code doesn’t have to store secrets or configuration values that can vary depending on where or how the application is run. Our code simply assumes that those values are available and correct.

Dotenv

The idea of storing configuration in the environment is simple for a single-app production environment, it is easy to set environment variables for the whole system.

Hosting providers like Heroku opens a new window or Render opens a new window have a configuration panel to manage the environment variables. However, when many applications have to run in the same system each of them may need different values for a given environment variable, and then the “environment” depends on the current project and not only on the system.

One of many tools to assist with this is the dotenv gem opens a new window , which wraps our application with specific environment values based on hidden files that can be loaded independently for each app without polluting the system’s environment variables.

The way dotenv works is that it will read environment variables names and values from a file named .env and will populate the ENV hash with them.

By default, dotenv will NOT override variables if they are already present in the ENV hash, but that can be changed using overload instead of load when initializing the gem opens a new window .

Sample or Template files

Since the .env file holds information that is specific for a given environment, this file is not meant to be included in the git repository.

How do we let new engineers know that we make use of a .env file or what the required environment variables are? The dotenv gem provides a good solution.

The dotenv gem provides a template feature opens a new window to generate a .env.template file with the same environment variables but without actual values.

Another common practice is to use a file called .env.sample with similar content.

When a new developer clones the repository, they can copy the .env.template or .env.sample file as .env (or any of the variants, we’ll talk about this in a moment) and replace the values as needed.

Dotenv-validator

One issue that we have faced in many projects is when a new developer would need to know the environment variables (listed in a .env.sample file), but wouldn’t know what to use as values that make sense.

In many cases any value works when the code doesn’t depend on the actual format of the value. However, when the data type or format does matter then things can go wrong.

One example we had for this issue was a third-party gem that required an API secret, the gem would verify the format of the secret against a regular expression and some actions would fail with an invalid secret format error.

To prevent this, we created and open-sourced the dotenv-validator gem opens a new window , which leverages the use of a .env.sample file with comments for every environment variable to provide extra information about the expected format of the value for each variable.

This gem includes a mechanism to warn an engineer about missing or incorrect environment variables when the application starts.

Dotenv-Rails

By default, dotenv only looks for a file named .env, but, when using dotenv-rails, it will provide some naming conventions that we can adopt to further differentiate the environment variables we use not only per app but also per Rails environment.

When running a Rails app with dotenv-rails, environment variable files are looked up in this order:

root.join(".env.#{Rails.env}.local"),
(root.join(".env.local") unless Rails.env.test?),
root.join(".env.#{Rails.env}"),
root.join(".env")

Using this convention we can specify different environment variables for the same application when we run the application with rails s or when we run the tests.

Note that all the files listed above are loaded and processed by dotenv in that specific order. This means you can have generic environment variables in a .env file and be more specific overriding/defining only some of them in a file for the current Rails environment without having to copy all the variables to the new file.

Foreman

New Rails application comes with a bin/dev script that uses the foreman gem opens a new window to run multiple processes at once. foreman is aware of the .env file and will load it before our application loads it. However, there’s one important difference, the way foreman parses the .env file is not the same as the way dotenv processes the same file.

The dotenv gem understands comments and they are ignored when setting the values in the ENV hash, while foreman does not ignore them. So, a .env file that looks like this:

MY_ENV="my value"    # some comment here

Will produce different values for ENV["MY_ENV"] depending on how the application is run:

  • when running the app directly with rails s, the comment is ignored by dotenv and ENV["MY_ENV"] returns the string "my value"
  • when running the app through foreman the comment is not ignored, so ENV["MY_ENV"] returns the string '"my value" # some comment here' (then, when the Rails app loads, the .env file is parsed again by dotenv but since the variable was already defined by foreman, it is not replaced)

One workaround for this is to rely on the naming convention of alternative files: if, for example, we use .env.development and .env.test files, these will only be parsed by dotenv thanks to the dotenv-rails convention and not by foreman.

Another option is to configure the initialization of dotenv to use overload instead of load.

Docker

Docker opens a new window is a really popular solution for containerizing applications, and Docker-related files will be created by Rails for new apps (since Rails 7.1) opens a new window .

When using docker-compose, it will look for a .env file and, in some cases, it may not ignore comments or even process the values differently than dotenv.

You can check the docs here opens a new window .

If environment variables are not populated correctly by docker-compose compared to dotenv, the workarounds used for foreman can be used here too.

Dotenv wrapper

Sometimes we have to run applications that are not aware of the .env file but do expect some configuration in the ENV hash. For example, a background job process running a worker that reads some information from the ENV hash.

In that case, instead of changing our job-runner code to load dotenv, we can use the dotenv executable to wrap any command. For example:

dotenv -f ".env.local" bundle exec rake sidekiq

This wrapper can then be used in a Procfile to ensure dotenv works as expected when using foreman for example if we don’t use a .env file.

Figaro

Another popular gem with a similar functionality is the figaro gem opens a new window . Compared to dotenv, figaro is focused more on Ruby on Rails applications and provides some features like ensuring the presence of specific environment variables (one of the features of dotenv-validator).

dotenv is not focused on Ruby on Rails applications (but can be used with no issues) and its development has been more active.

In Conclusion

Because of the work we do at OmbuLabs with multiple clients, handling environment variables with a .env file is key for us to quickly change between projects locally without polluting the system’s environment variables.

For our projects we don’t use a .env file in production, since we define the environment variables in the Heroku dashboard, but we still use dotenv-validator to ensure that the application has all the variables with correct values to avoid unexpected issues.

We try to keep the .env.sample file with development-ready values, but it’s not always possible when some variables can be specific for a machine or developer, so adding format validation can help the developer set the correct value.

Feel free to reach out to OmbuLabs for help in your project, we offer many types of services opens a new window .