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.
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
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
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
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
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
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"
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
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
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
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.
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.