Working with Environment Variables in Ruby
Published on November 14, 2023

When working with web applications, developers need to handle lots of different moving parts. Most of the time we don't really stop and think about what those individual parts are and exactly how they work. In this article, we're going to explore just one of these parts: environment variables.

If you've ever built and deployed a Ruby web application you've probably used environment variables. If that application was built with Ruby on Rails, then you'll have needed to set RAILS_ENV to 'production' during the deployment process. If the application integrated external services or APIs, then you will likely have used environment variables to manage the credentials for those services.

But what exactly are environment variables, why are they useful, and how can we leverage them when developing and deploying Ruby applications?

What Are Environment Variables?

As the name suggests, environment variables are variables that store information about the environment that our application operates in. You're probably already familiar with the concept of variables and how they work, so let's look a little more closely at that second part: environment.

The environment in this context can refer to the operating system on which your program executes, but can also refer to the process, or processes, used to execute the program. This might become a little bit clearer if we look at some examples.

When you open a Terminal window on your computer, this starts up a Shell process. A Shell is essentially a program that allows you to interact with your system by processing commands issued via the Shell and outputting the result. For example, when booting up a Rails application on your local machine, you issue a command like rails s and see output something like this:

=> Booting Puma
=> Rails 7.0.4.3 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.5 (ruby 3.0.0-p0) ("Birdie's Version")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 126917
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop

This process, the Shell, knows things about the environment that it is executed in, and it stores these things that it knows in variables. Processes inherit a copy of the environment variables from their parent process, so the Shell has a copy of the operating system's (or more specifically the kernel's) environment variables. Similarly, any process executed in the Shell will inherit a copy of that Shell's environment variables.

Accessing Environment Variables

On UNIX-based systems (e.g. Linux and OSX) you can access all of the environment variables of the Shell by using the env command. Windows has an equivalent set command, but for the purposes of this article, we'll only cover commands used in UNIX-based systems.

If you want to print out a specific environment variable, you can use the printenv command. For example, if I wanted to check the language I have set on my system I can issue the printenv command followed by the name of the environment variable which contains this data, in this case LANG. This will output the language, which on my system is en_GB.UTF-8:

$ printenv LANG
en_GB.UTF-8

A more Ruby-centric example would be RUBY_VERSION:

$ printenv RUBY_VERSION
ruby-3.2.2

If you were to execute a Ruby process from your Shell, this is the version of Ruby that the process would use. If you have a Ruby version manager installed, such as chruby, rbenv, or rvm, try running the printenv RUBY_VERSION then changing your Ruby version with your version manager and running printenv RUBY_VERSION again. You should see the Ruby version that you just changed to.

Speaking of Ruby processes, we can access environment variables from within a Ruby program via Ruby's ENV object. As the Ruby documentation explains:

ENV is a hash-like accessor for environment variables.

The ENV class has several different methods that allow you to interact with the values stored within an ENV object. For the purposes of this article, we're only interested in accessing values using bracket notation but feel free to explore the rest of the functionality outlined in the Ruby docs.

We can access the value of an environment variable via ENV via bracket notation, the same way that we would access values from a Ruby Hash, using the name of the environment variable as the key.

ENV['RUBY_VERSION'] # => ruby-3.2.2

Note that pass the variable name, RUBY_VERSION, as a String to the ENV brackets.

Let's test this out in our Shell:

$ printenv RUBY_VERSION
ruby-3.2.2
$ ruby -e "puts ENV['RUBY_VERSION']"
ruby-3.2.2

In the above example, we first use the printenv command to output the value of the Shell's RUBY_VERSION environment variable, which is ruby-3.2.2. We then invoke the ruby command with the -e option. This tells the Ruby interpreter not to execute the string we then pass it as Ruby code. This Ruby code outputs the value associated with the ENV object's RUBY_VERSION key. As we can see, the value for 'RUBY_VERSION' inside of the Ruby process is the same as the value for RUBY_VERSION inside its parent Shell process.

Setting Environment Variables

So far we've only explored how to access existing environment variables. They become even more useful though, when you start setting your own.

In a UNIX-based Shell, environment variables can be set using the export command combined with assignment syntax:

$ export FOO=bar
$ printenv FOO
bar

As with pre-existing environment variables, those that you create within a process are also inherited by any child processes. Again, we can test this out within a Ruby process.

$ export FOO=bar
$ printenv FOO
bar
$ ruby -e "puts ENV['FOO']"
bar

An important thing to note is that sibling processes maintain their own copies of environment variables rather than sharing them. If you open a second Terminal window and execute printenv FOO, nothing gets output.

On a similar note, environment variables created within a process die with that process. For example, if you execute export FOO=bar, close the Terminal window, then open a new Terminal window and execute printenv FOO, nothing gets output.

Why Use Environment Variables?

One of the primary uses for environment variables is in setting configuration data when developing or deploying an application. A common example of this is for setting API credentials when using an external service such as the Vonage Communications APIs.

Say, for example, you have a Ruby file called send_sms.rb. The code in this file sends an SMS via the Vonage SMS API using the Vonage Ruby SDK. To authenticate your request to the Vonage SMS API, you need to instantiate a Vonage::Client object with an api_key and api_secret.

client = Vonage::Client.new(api_key: 'abc123', api_secret: 'ab1CDef2GhIjkLmn')

client.sms.send(from: 'Ruby', to: '447700900000', text: 'Hello world')

Generally, you wouldn't want to hard-code these credentials into your source code like this. You'll likely be committing that code to a version control service like GitHub; even if the repository is not public, it's not really a good idea to expose your API credentials in your Git history. Additionally, you might well be using different API credentials in development than you are in production (and possibly across other environments as well, such as staging or QA). Environment variables can be updated between deploys without having to modify any of the source code.

In the context of a Ruby application, this is where we can leverage Ruby's ENV object. Our updated send_sms.rb code might look something like this:

client = Vonage::Client.new(api_key: ENV['VONAGE_API_KEY'], api_secret: ENV['VONAGE_API_SECRET'])

client.sms.send(from: 'Ruby', to: '447700900000', text: 'Hello world')

You could then use export to set VONAGE_API_KEY and VONAGE_API_SECRET as environment variables:

$ export VONAGE_API_KEY=abc123 VONAGE_API_SECRET=ab1CDef2GhIjkLmn

When you subsequently run the send_sms.rb file, the ENV object in the Ruby process executing the code will have access to the environment variables you set.

$ ruby send_sms.rb

As a sidenote, if you don't pass the api_key and api_secret keyword arguments to Vonage::Client.new, when necessary the Vonage Ruby SDK will automatically check the ENV object for environment variables named VONAGE_API_KEY and VONAGE_API_SECRET. As long as you have these environment variables set when using Vonage APIs that require an API key and secret for authentication, you can instantiate the Vonage::Client object like this:

client = Vonage::Client.new

How to Use Environment Variables

In the examples so far, you've used export to set your environment variables. Doing this every time you execute a Ruby process in a new Shell or environment can become a bit laborious, especially if you have a large number of environment variables to set. Luckily there are other solutions available for both development and production.

In Development

A great option in development is the dotenv library. This is a RubyGem that you can include in your Gemfile or install locally. To use it, you need to create a .env file in the root of your Ruby project. Within that file, you define the environment variables that your application requires as key-value pairs.

VONAGE_API_KEY=abc123
VONAGE_API_SECRET=ab1CDef2GhIjkLmn

In your Ruby application, you can then require the gem and call the load method on the Dotenv class. This loads all the environment variables defined in your .env file into the ENV object for the current Ruby process.

require 'dotenv'
Dotenv.load

client = Vonage::Client.new(api_key: ENV['VONAGE_API_KEY'], api_secret: ENV['VONAGE_API_SECRET'])

client.sms.send(from: 'Ruby', to: '447700900000', text: 'Hello world')

There's also a dotenv-rails gem included as part of the library, which is specifically for use with Rails applications. The usage is slightly different from the standard dotenv gem, but the concept is the same.

The library has some other functionality, which I won't cover here, but you can read about it in the documentation.

One final point here, whether using dotenv or dotenv-rails you should always make sure that you add your .env file to .gitignore so that it won't be checked into your Git repository.

In Production

Although, according to the documentation you can use dotenv in production, other tools and options are better suited to managing environment variables in production.

  • Configuration Management tools such as Chef, Puppet, and Ansible are powerful, fully-featured options. These tools are probably best suited to working at scale, but are probably over-the-top for smaller projects if all you want to do is set a few environment variables.

  • A containerization solution like Docker provides a specific way of handling environment variables for sensitive data such as API keys.

  • If you're using a service such as Render or Heroku to deploy and host your Ruby applications, you can generally set your environment variables as key-value pairs within the UI provided by the service, whether that's for a single application or a group of applications (see the screenshot below). Some commonly used environment variables may even be set for you; for example, Render automatically sets RAILS_ENV to production for Ruby applications.

Screenshot of the Render Dashboard for adding environment variables, showing a form with 'key' and 'value' fieldsRender Environment Variable form

A Brief Note on Rails Credentials

If you are working specifically with Rails, an alternative to using environment variables is to use Rails Credentials, though that's a topic for another blog post!

Wrapping Up

That's it for now! I hope you found this article interesting and informative. If you have any comments or suggestions, feel free to reach out to us on X (formerly known as Twitter) or drop by our Community Slack. If you enjoyed it, please check out our other Ruby articles.

Karl LingiahRuby Developer Advocate

Karl is a Developer Advocate for Vonage, focused on maintaining our Ruby server SDKs and improving the developer experience for our community. He loves learning, making stuff, sharing knowledge, and anything generally web-tech related.

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.