Flexible Rails Environment Configuration

It’s hard to write a Rails app without interfacing with an external service or API. In most cases, these services require a secret token or password. Since checking passwords in to source control is generally a bad practice, we need a good way to safely and reliably access these values in code.

I’ve tried a few gems that attempt to make this process easier and ultimately settled on a simpler solution - the combination of a few lines of code and ENV variables, which are accessible whether you manage your own infrastructure or use a PaaS service like Heroku.

The Problem

In a recent Rails app, I used Pusher and it requires the unique URL to be setup in an initializer:

# config/initializers/pusher.rb

Pusher.url = "http://asdfa@api.pusherapp.com"

If I need to set this URL in multiple place - copying/pasting is not a great solution because it sets me up for copy/paste errors. It’s also a headache to find all occurrences of the URL when it changes.

The Solution

Setup a yaml file similar to this:

# config/application.yml

defaults: &defaults
  PUSHER_URL: http://asdfa@api.pusherapp.com

development:
  <<: *defaults

test:
  <<: *defaults

Add the following to config/application.rb before the Rails application class is defined:

if File.exists?(File.expand_path('../application.yml', __FILE__))
  config = YAML.load(File.read(File.expand_path('../application.yml', __FILE__)))
  config.merge! config.fetch(Rails.env, {})
  config.each do |key, value|
    ENV[key] ||= value.to_s unless value.kind_of? Hash
  end
end

Resulting in a config/application.rb that looks something like this:

# config/application.rb

require File.expand_path('../boot', __FILE__)

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env)

if File.exists?(File.expand_path('../application.yml', __FILE__))
  config = YAML.load(File.read(File.expand_path('../application.yml', __FILE__)))
  config.merge! config.fetch(Rails.env, {})
  config.each do |key, value|
    ENV[key] ||= value.to_s unless value.kind_of? Hash
  end
end

module NewApp
  class Application < Rails::Application
    # ...
  end
end

The added code first looks for an application.yml file. If it finds one, it reads in the values for that environment (development, test, production, etc.) and merges them in to the existing Ruby ENV hash.

Note: Since application.yml will typically hold passwords and other secret keys, it should be added to .gitignore so they don’t end up in source control.

Now, when the application is initialized, PUSHER_URL is available in the ENV hash. This allows me to the update the pusher initializer to:

# config/initializers/pusher.rb

Pusher.url = ENV["PUSHER_URL"]

If I’m deploying to Heroku, I need to set the config value using the following command:

heroku config:set PUSHER_URL=http://asdfa@api.pusherapp.com

Or if I’m deploying to Ubuntu, I can set the ENV variable for all users on the system:

# /etc/environment

...
PUSHER_URL=http://asdfa@api.pusherapp.com

Note: If you choose not to set ENV variables on the host system, you can easily mimic the development environment and drop the config/application.yml file on to the system. This is no different than using a database.yml that most Rails developers are used to.

That’s it! So with 7 lines of code and a configuration file, you have yourself an environment configuration setup that’s flexible for most deployment solutions.