In Progress
Unit 1, Lesson 21
In Progress

Dotenv

Video transcript & code

In the previous episode I briefly mentioned a gem called Dotenv. Today I want to cover it in a little more detail, because it's really quite cool.

Very often we find a need to pass small bits of configuration data into an application. One of the most common examples of this is an API key for some external service. We don't want to hardcode this value, because individual developers should be using their own API keys. They shouldn't have access to the production API key.

The simplest way to configure these types of simple variable on a per-machine basis is to make them environment variables. The system environment gives us a basic key-value store without having to deal with managing and parsing config files. And for some application hosts such as Heroku, environment variables are the only supported way to set configuration values without committing them to the source code repository in some form.

api_key = ENV["YOYODYNE_API_KEY"]

This is fine for staging and production servers. But when we're running the code locally as developers, it's less than ideal. We have to remember to set the API keys before running the application, or we have to put these per-app keys in our global shell init files, or one of several other inconvenient schemes.

$ export YOYODYNE_API_KEY=SECRET123
$ rails s

This is where the Dotenv gem comes in. We start by adding it to our Gemfile. If we're working on a Rails app, we can make a dependency on the dotenv-rails gem, and we're done setting it up.

gem "dotenv-rails", :groups => [:development, :test]

Note that it is typically only needed in the development and test environments, since it's assumed that in production the environment variables will already be set.

For other kinds of application, we depend on the base dotenv gem.

gem "dotenv"

Then we find a spot as early as possible in our app startup sequence to initialize Dotenv. For instance, the Rubytapas.com Sinatra app, I have a file called environment which is loaded before anything else.

require_relative 'environment'

In that file, I require dotenv and then invoke Dotenv.load.

require 'dotenv'
Dotenv.load(File.expand_path("../.env",  __FILE__))

In this case I've specified an explicit filename for it to load, but we can also call it with no arguments and it will find a file called .env in the current directory.

require 'dotenv'
Dotenv.load

For demonstration purposes, let's jump over to an empty project. We'll require "dotenv" and tell it to load, just like before.

require 'dotenv'
Dotenv.load

Now we'll create a .env file. Inside, we can make simple key-value assignments. We'll set up an API key variable.

YOYODYNE_API_KEY=SECRET123

Back in the code, we'll access the the environment variable YOYODYNE_API_KEY.

require "dotenv"
Dotenv.load

api_key = ENV["YOYODYNE_API_KEY"]
api_key # => "SECRET123"

Lo and behold, it has the value we set in the .env file. This is what Dotenv does: it finds the variable file, and updates the applications global environment using the definitions it finds there.

Now let's try setting the environment variable before loading up Dotenv.

ENV["YOYODYNE_API_KEY"] = "PRODUCTION_KEY"

require "dotenv"
Dotenv.load

api_key = ENV["YOYODYNE_API_KEY"]
api_key # => "PRODUCTION_KEY"

This simulates the production environment where the environment variables are always configured before the application starts. We can see that this time, the pre-set value overrides the .env value. Dotenv only takes effect when an environment variable is missing.

If the keys in the .env file are specific to each developer, then we should keep this file out of revision control, for instance by adding it to our .gitignore file. On the other hand, if there are some common API keys shared among developers, we can check it into our repo.

There's one other Dotenv feature worth mentioning. Very often, project Rakefiles also need environment variables to be configured. Dotenv has special support for Rake to make this easier. In our Rakefile we can just require dotenv/tasks. Then we make any task that needs the environment variables loaded up depend on the special :dotenv task.

require "dotenv/tasks"

task :my_task => :dotenv do
  # ...
end

Remembering the correct environment variables to set before starting up an application has always been an annoying speed-bump for me. Dotenv smoothes over that speed bump, and as a result it has become one of my most essential tools.

Happy hacking!

Responses