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 created The Twelve-Factor App 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 or Render 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 , 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 .
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 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 , 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 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 bydotenv
andENV["MY_ENV"]
returns the string"my value"
- when running the app through
foreman
the comment is not ignored, soENV["MY_ENV"]
returns the string'"my value" # some comment here'
(then, when the Rails app loads, the.env
file is parsed again bydotenv
but since the variable was already defined byforeman
, 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 is a really popular solution for containerizing applications, and Docker-related files will be created by Rails for new apps (since Rails 7.1) .
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 .
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 . 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 .