The Rubyist's Guide to Environment Variables

If you want to be able to effectively manage web apps in development and in production, you have to understand environment variables. This post will show you how environment variables really work - and perhaps more importantly, how they DON'T work. We'll also explore some of the most common ways to manage environment variables in your Rails apps.

If you want to be able to effectively manage web apps in development and in production, you have to understand environment variables.

This wasn't always the case. Just a few years ago, hardly anyone was configuring their Rails apps with environment variables. But then Heroku happened.

Heroku introduced developers to the 12-factor app approach. In their 12-factor app manifesto they lay out a lot of their best practices for creating apps that are easy to deploy. The section on environment variables has been particularly influential.

The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard.

More Rubyists are using environment variables than ever. But often it's in a cargo-culty way. We're using these things without really understanding how they work.

This post will show you how environment variables really work - and perhaps more importantly, how they DON'T work. We'll also explore some of the most common ways to manage environment variables in your Rails apps. Let's get started!

NOTE: You can read about securing environment variables here.

Every process has its own set of environment variables

Every program you run on your server has at least one process. That process gets its own set of environment variables. Once it has them, nothing outside of that process can change them.

An understandable mistake that beginners make is to think that environment variables are somehow server-wide. Services like Heroku sure make it seem like setting the environment variables is the equivalent of editing a config file on disk. But environment variables are nothing like config files.

Every program you run on your server gets its own set of environment variables at the moment you launch it.

Every process has its own environment. Every process has its own environment.

Environment variables die with their process

Have you ever set an environment variable, rebooted and found that it was gone? Since environment variables belong to processes, that means whenever the process quits, your environment variable goes away.

You can see this by setting an environment variable in one IRB session, closing it, and trying to access the variable in a 2nd irb session.

When a process shuts down, its environment variables are lost When a process shuts down, its environment variables are lost

This is the same principal that causes you to lose environment variables when your server reboots, or when you exit your shell. If you want them to persist across sessions, you have to store them in some kind of configuration file like .bashrc   .

A process gets its environment variables from its parent

Every process has a parent. That's because every program has to be started by some other program.

If you use your bash shell to launch vim, then vim's parent is the shell. If your Rails app uses imagemagick to identify an image, then the parent of the identify program will be your Rails app.

Child processes inherit env vars from their parent Child processes inherit env vars from their parent

In the example below, I'm setting the value of the $MARCO environment variable in my IRB process. Then I use back-ticks to shell out and echo the value of that variable.

Since IRB is the parent process of the shell I just created, it gets a copy of the $MARCO environment variable.

Environment variables set in Ruby are inherited by child processes Environment variables set in Ruby are inherited by child processes

Parents can customize the environment variables sent to their children

By default a child will get copies of every environment variable that its parent has. But the parent has control over this.

From the command line, you can use the env program. And in bash there's a special syntax to set env vars on the child without setting  them on the parent.

Use the env command to set environment variables for a child without setting them on the parent Use the env command to set environment variables for a child without setting them on the parent

If you're shelling out from inside Ruby you can also provide custom environment variables to the child process without littering up your ENV hash. Just use the following syntax with the system method:

How to pass custom environment variables into Ruby's system method How to pass custom environment variables into Ruby's system method

Children can't set their parents' environment variables

Since children only get copies of their parents' environment variables, changes made by the child have no effect on the parent.

Environment variables are "passed by value" not "by reference" Environment variables are "passed by value" not "by reference"

Here, we use the back-tick syntax to shell out and try to set an environment variable. While the variable will be set for the child, the new value doesn't bubble up to the parent.

Child processes can't change their parents env vars Child processes can't change their parents env vars

Changes to the environment don't sync between running processes

In the example below I'm running two copies of IRB side by side. Adding a variable to the environment of one IRB session doesn't have any effect on the other IRB session.

Adding an environment variable to one process doesn't change it for other processes Adding an environment variable to one process doesn't change it for other processes

Your shell is just a UI for the environment variable system.

The system itself is part of the OS kernel. That means that the shell doesn't have any magical power over environment variables. It has to follow the same rules as every other program you run.

Environment variables are NOT the same as shell variables

One of the biggest misunderstandings happens because shells do provide their own "local" shell variable systems. The syntax for using local variables is often the same as for environment variables. And beginners often confuse the two.

But local variables are not copied to the children.

Environment variables are  not the same as shell variables Environment variables are not the same as shell variables

Let's take a look at an example. First I set a local shell variable named MARCO. Since this is a local variable, it's not copied to any child processes. Consequently, when I try to print it via Ruby, it doesn't work.

Next, I use the export command to convert the local variable into an environment variable. Now it's copied to every new process this shell creates. Now the environment variable is available to Ruby.

Local variables aren't available to child processes. Export converts the local variable to an environment variable. Local variables aren't available to child processes. Export converts the local variable to an environment variable.

Managing Environment Variables in Practice

How does this all work in the real world? Let's do an example:

Suppose you have two Rails apps running on a single computer. You're using Honeybadger to monitor these apps for exceptions. But you've run into a problem.

You'd like to store your Honeybadger API key in the $HONEYBADGER_API_KEY environment variable. But your two apps have two separate API keys.

How can one environment variable have two different values?

By now I hope you know the answer. Since env vars are per-process, and my two rails apps are run in different processes there's no reason why they can't each have their own value for $HONEYBADGER_API_KEY.

Now the only question is how to set it up. Fortunately there are a few gems that make this really easy.

Figaro

When you install the Figaro gem in your Rails app, any values that you enter into config/application.yml will be loaded into the ruby ENV hash on startup.

You just install the gem:

# Gemfile
gem "figaro"

And start adding items to application.yml. It's very important that you add this file to your .gitignore, so that you don't accidentally commit your secrets.

# config/application.yml

HONEYBADGER_API_KEY: 12345

Dotenv

The dotenv gem is very similar to Figaro, except it loads environment variables from .env, and it doesn't use YAML.

Just install the gem:

# Gemfile

gem 'dotenv-rails'

And add your configuration values to .env - and make sure you git ignore the file so that you don't accidentally publish it to github.

HONEYBADGER_API_KEY=12345

You can then access the values in your Ruby ENV hash

ENV["HONEYBADGER_API_KEY"]

You can also run commands in the shell with your pre-defined set of env vars like so:

dotenv ./my_script.sh

Secrets.yml?

Sorry. Secrets.yml - though cool - doesn't set environment variables. So it's not really a replacement for gems like Figaro and dotenv.

Plain old Linux

It's also possible to maintain unique sets of environment variables per app using basic linux commands. One approach is to have each app running on your server be owned by a different user. You can then use the user's .bashrc to store application-specific values.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Starr Horne

    Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

    More articles by Starr Horne
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial