Understanding Rails Environments

💬 Understanding How Rails Environments Work

This post covers the basics of environments in Rails, including what they are, how they work and why they're so important. We'll also take a look behind the scenes and dive into the Rails source code to learn exactly how they're implemented, exploring a few interesting "inquiry" classes in Rails.

6 min read

Rails applications typically run in one of three environments: development, test, and production. In addition, you can create your own custom environment, such as qa or staging.

Each of these environments has its own configuration settings, which allows your application to behave differently based on the environment it’s running in. For example, your app can log to standard output in development, whereas it might use an external logging service in production.

How does Rails know which environment to use?

To figure out the current environment, Rails uses the RAILS_ENV environment variables. If that’s not set, it will use the RACK_ENV variable. You can set RAILS_ENV to one of the following values to choose the correct environment: development test, or production.

To set the Rails environment, you’d use the following command in a terminal (or in a .bashrc or .zshrc file)

$ export RAILS_ENV=production

Alternatively, you can specify the environment name by passing the -e flag to Rails commands.

$ bin/rails server -e staging

You can also set the environment in your Ruby code. In fact, that’s what the default test_helper does in a Rails application.

# test/test_helper.rb

ENV["RAILS_ENV"] ||= "test"

Note: If you don’t set any of the above environment variables, Rails will assume the development environment as a default.

For each of the above environments, Rails provides a dedicated configuration file stored under the config/environments directory. If you open a newly minted Rails application, you will notice three files in there: development.rb, test.rb, and production.rb, containing the configuration settings for each environment.

Put the configuration settings common to all environments in the config/application.rb file, and settings specific to a particular environment in the config/environments/#{env}.rb file.

💡
Environment config files take precedence over the application config.

🤔 Which environment I'm using?

To figure out the current environment your application is running under, use the Rails.env method, which returns a string containing the name of the environment.

Rails.env  # "development"

Alternatively, you can use the query methods named after the environments.

Rails.env.production?  # false
Rails.env.test?        # true

To create and configure a new environment, say “staging”, you’d simply add a new config/environments/staging.rb file with its own settings. This new environment is no different than other. You can start a server with bin/rails server -e staging, launch a console with bin/rails console -e staging, and check the environment using Rails.env.staging? method.

But you probably knew all of that. So let’s learn something new and interesting by taking a peek behind the scenes to understand how it's implemented.

Behind the Scenes of Rails Environments

The env method is defined in the railties/lib/rails.rb file. It returns the current Rails environment. Here’s the implementation:

# railties/lib/rails.rb

module Rails
  class << self
    def env
      @_env ||= ActiveSupport::EnvironmentInquirer.new(
        ENV["RAILS_ENV"].presence || 
        ENV["RACK_ENV"].presence || 
        "development"
      )
    end
  end
end

As you can see, Rails lazily sets the value of the @_env variable to an instance of the EnvironmentInquirer class, passing the values of RAILS_ENV, RACK_ENV, and "development" in that order.

The EnvironmentInquirer class takes the name of the environment, i.e. "production", "test", or "development" and does two things:

  1. Set the corresponding instance variable to true or false if it matches the passed environment name.
  2. Add query methods like test? or production? returning the above value. This allows you to query the environment like Rails.env.development? and so on.

Here’s the simplified source code.

# activesupport/lib/active_support/environment_inquirer.rb

module ActiveSupport
  class EnvironmentInquirer < StringInquirer
    DEFAULT_ENVIRONMENTS = %w[ development test production ]

    def initialize(env)
      DEFAULT_ENVIRONMENTS.each do |default|
        instance_variable_set :"@#{default}", env == default
      end
      @local = in? LOCAL_ENVIRONMENTS
    end

    DEFAULT_ENVIRONMENTS.each do |env|
      class_eval <<~RUBY, __FILE__, __LINE__ + 1
        def #{env}?
          @#{env}
        end
      RUBY
    end
  end
end

In addition, it also adds a local? query method that returns true if the current environment is development or test. This is a handy shortcut if you want to skip running some code in production, or run something only in production.

Note: If you don't know how class_eval works in Ruby, I highly recommend reading Paolo Perrotta's Metaprogramming Ruby 2.

Metaprogramming in Ruby
Metaprogramming in Ruby enables you to produce elegant, clean, and beautiful programs as well as unreadable, complex code that’s not maintainable. This book teaches you the powerful metaprogramming concepts in Ruby, and how to use them judiciously.

What is a String Inquirer?

You must have noticed that the EnvironmentInquirer class inherits from the StingInquirer class. What’s going on?

The StringInquirer class provides a query- based way to compare two strings. Here’s a simple example:

def result
  ActiveSupport::StringInquirer.new("success")
end

result.success?   # true
env.failure?      # false

This lets you query the env method against any other environment name, such as Rails.env.staging? or Rails.env.qa?.

In fact, we didn’t even have to define the DEFAULT_ENVIRONMENTS constant above and EnvironmentInquirer would have handled the default environments. It’s an optimization for the three default environments, so this inquirer doesn't need to rely on the slower delegation through method_missing that StringInquirer would normally use.

To learn more about string and array inquries, check out the following posts.

Compare Strings Using Inquiry
The ActiveSupport::StringInquirer class provides a nice API to compare two strings.
Array Inquiry in Rails
The ArrayInquirer class provided by the Active Support framework in Rails provides a readable way to check the contents of an array. This post explores how you can implement this using metaprogramming in Ruby.

Override Existing Configuration

Sometimes, you may want to create a new configuration that mimics existing configuration, with only one or two settings being different. For example, the staging environment typically matches the production environment, with only difference being the database connection.

In these cases, you can require the existing configuration and override it in-place as follows.

# config/environments/staging.rb
require_relative "production"

Rails.application.configure do
  # config settings specific to
  # the staging environment.
end

Bonus: Did you know about the bin/rails about command?

$ bin/rails about

About your application's environment
Rails version             7.1.2
Ruby version              ruby 3.2.3 (2024-01-18 revision 52bb2ac0a6) [x86_64-darwin21]
RubyGems version          3.4.19
Rack version              3.0.8
Middleware                ActionDispatch::HostAuthorization, Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, ActionDispatch::ServerTiming, ...
Application root          /Users/akshay/blog
Environment               development
Database adapter          sqlite3
Database schema version   20240111083326

You're welcome 😉

Further Reading

  1. Check out all the available settings for various Rails sub-frameworks on the official Rails Guides on Configuring Rails Applications.
  2. This article covers some of the important configuration settings that are common to most Rails applications: Configuring Rails Environments
  3. For an interesting read on how Basecamp used Rails environments other than the default ones, check out this article Beyond the default Rails environments from David. It’s over a decade old, but interesting nonetheless.

That's a wrap. I hope you found this article helpful and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.