Good apps are built following the The Twelve-Factor App principles and we’ve been adhering to those for some time now; problem is: Ruby on Rails doesn’t always follow those principles in favor of ease of use and simplicity. One aspect that can be improved is configuration through environment variables.
ENV variables FTW
Before digging into the details, let’s discuss a little bit about why environment variables are good for you:
- code is identical no matter where you run it;
- configuration is dynamic and not stored statically within code;
- you don’t need a commit to change a setting;
- production configuration will be stored safely in a separate place;
- environments are just a list of variables and are easy to replicate.
Of course the above can be obtained within what Rails already suggests, but fallacy is right around the corner and it’s more tempting to just commit everything to the repository when using “The Rails Way”.
The Rails way
As the RailsGuides explains, Ruby on Rails is mostly configured
via static YAML files (like
database.yml) and initializer files.
Some configurations can be handled via an environment variable (like
DATABASE_URL) but not much can be done with those, too few are available
to actually configure a full application.
Why is this so popular:
- ease of use;
- really simple to understand;
- powerful mix of configuration and Ruby code;
- configuration is validated at app boot.
What are the limits of this approach:
- code duplication;
- potential configuration leak;
- configuration could be committed in repo;
- infrastructure must be built around the app;
- initial setup of app requires
cp some.yml.example some.yml.
Another thing worth mentioning that you can do in Rails is using encrypted credentials, but I find them almost impossible to work with: they feel like encryption has been used as a trade-off to dump every possible environment configuration within the application code without worrying too much.
Over the years we’ve matured an approach that uses environment variables but still makes it possible to go through Ruby on Rails to get the best of both worlds.
We use config_for to load a
config/settings.yml that is actually
commited to the repo but loads configuration from ENV. An example file
default: &default secret_key_base: <%= ENV.fetch('SECRET_KEY_BASE', 'some-default') %> host: <%= ENV.fetch('HOST', 'localhost:3000') %> s3: assets_bucket: <%= ENV.fetch('S3_BUCKET') %> access_key_id: <%= ENV.fetch('S3_ACCESS_KEY') %> secret_access_key: <%= ENV.fetch('S3_SECRET_ACCESS_KEY') %> region: <%= ENV.fetch('S3_REGION', 'us-west-1') %> development: <<: *default test: <<: *default production: <<: *default
And we load this file with a custom helper built this way:
module Nebulab class Config class << self def fetch(key) settings = Rails.application.config_for(:settings) key.split('.').inject(settings) do |accumulator, subkey| accumulator.fetch(subkey) end end end end end
So for example if you need to load a configuration within the application it’s as simple as:
Common mistake: use dotenv to manage configurations
Some may ask: why can’t you simply use dotenv to manage app configurations?
Well, for starters, dotenv is not a gem meant for application configuration
management, but a gem that loads environment variables from an
On multiple occasions I’ve seen dotenv used as a configuration manager and I’ve
always seen developers fail spectacularly at keeping configurations in order.
In the end, using only dotenv for managing configurations is not worth it. We find our approach has some added benefits:
- it’s still plain Rails, no need for an extra gem;
- the configuration is validated at application boot;
- app boot will fail if a required variable hasn’t been provided;
- defaults are good for working on the app locally;
- developers won’t have access to production configurations;
- you get a manifest of the required configurations;
So if you want to use dotenv, use it just to load in environment variables!
From our experience, this way of storing configurations for apps combines the best of both worlds and it has been battle tested on modern Docker-centered environments as well as traditional Capistrano-centered infrastructures.
That’s all! Do you have any preferred way of storing configurations? We’d like to know. Got questions? Leave a comment!