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
toproduction
for Ruby applications.
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.