Secrets of the Rails Boot process

2017-12-12 • By John McDowall

Have you ever wondered exactly how Rails comes to life when you run bin/rails s? Understanding what the boot-up process is can help you gain a deeper understanding of how Rails works, how it does things like per-environment configuration, and how you can tweak the bootup process.

First, let's talk about Rack

Rack is a web server interface specification first devised around May 2007, over 10 years ago, and can be safely considered mature. The interface as defined is extremely minimal.

All one has to do is provide an 'App' which is an object responding to one method call and accepts an environment hash, and returns an array with three elements: the HTTP response code, a hash of HTTP headers, and the HTTP response body.

Since in Ruby, a proc responds to call by default, you can create a Rack app with just one proc:

require 'rack'

app = Proc.new do |env|
    ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end

Rack::Handler::WEBrick.run app

You could save the previous snippet to a file config.ru (the .ru extension is convention for Rackup files) and use the rackup command on the CLI to startup your barebones Rack app:

rackup config.ru

Running curl http://localhost:8080 on the CLI will show you the message from your App: 'A barebones rack app.'.

Rails uses the same mechanism to boot up and handle requests, because the Rails structures your app as a Rack application.

Your Rails App is just a Rack application

If you create a new Rails app, or take a look at any older Rails app, in the root directory you'll quickly find a config.ru file. You've probably never looked inside it, but here's the minimal contents:

# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

run Rails.application

In fact, you can even use the rackup command in your Rails project root to launch a server and load Rails, because automatically this config.ru file is going to be loaded and executed.

Rails includes rack as a dependency, which is why the rackup command is available to you if you've installed Rails.

Ignoring the comment on the first line, the next piece of actionable code is the require_relative 'config/environment' statement.

The next statement kicks off Rails: run Rails.application.

Examining config/enviroment.rb

Looking at this file reveals another terse set of commands:

# Load the Rails application.
require_relative 'application'

# Initialize the Rails application.
Rails.application.initialize!

Straightforward. let's continue to follow the trail.

Examining config/application.rb

In this file, we get to something substantial:

require_relative 'boot'

require 'rails/all'

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

module MyRailsApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.1

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.
  end
end

The first line require_relative 'boot' is responsible for loading the config/boot.rb file, which sets up Bundler so that all of the Gems specified in your Gemfile are loaded and setup:

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)

require 'bundler/setup' # Set up gems listed in the Gemfile.

Next we have require 'rails/all'. This is the line responsible for loading 'Rails the Framework` itself: ActiveRecord, ActiveSupport, ActionView etc.

If we take a look at all.rb we can see it's nothing more than a glorified require processor for all the various sub-systems of Rails.

If you wanted to slim Rails down to not include, for example, ActiveJob at startup, you could replace require 'rails/all' with a specific set of requires for each sub-system that you want.

The next line of note informs Bundler to require the Gems specified for the current Rails environment, development, test, production and so on.

At this point, the next notable chunk of code is an Application class definition. It's here that Rails' main Application class is sub-classed into your project, and gives you the opportunity to further customize Rails.

While you could add configuration inside the Application class, any configuration defined there is overridden later on by either Rails Initializers, or Environment config files, therefore it isn't a great place to put that type of configuration.

Back to config/environment.rb

Since Ruby has just loaded the config/application.rb file, it's given Rails the chance to load all of it's sub-systems, define any specific configuration and so on.

Now it's show time, and we see the final line in config/environment.rb kicks everything off:

Rails.application.initialize!

Where was the call method for Rack to use?

Back in config/application.rb we saw that our Application class inherits from Rails::Application. If we take a look at the Rails source for application.rb we can see that the Rails Application class in turn inherits from the Engine class.

It's then a simple matter to look at the Rails source for engine.rb and see that Engines implement a Rack interface. The call method is on line 522.

From this we can see that Rails has structured your Application as a Rails Engine, which implements the Rack interface, from the start.

Summary

We took a brief look at how Rails boots up, and learned the following:

  • Rack apps implement at least one method: call
  • Rails Engines are rack applications, implementing call
  • Rails structures your Application as a Rails Engine

Hopefully this article has given you a better idea of how Rails boots up, what's involved, and where your environment and initializers fit into that process.

Get notified about new Blog posts

Sign up to our low volume newsletter and you'll hear straight away when we publish new blog posts.