Rails 6 boot sequence

By Younes SERRAJ

Have you ever wondered how your Rails application boots? I mean, when you execute rails server, what happens?

To answer this question, we’re going to start by generating a new Rails 6 application (I’m currently running 6.0.0.rc1).

$ rails new iluvrails
$ cd iluvrails

The starting point of the boot sequence is the rails executable. To simplify this blog, we’ll start our journey in ./bin/rails.

By the way, what is the rails gem? It’s a packaging for all the following:

$ gem dependency rails -v 6.0.0.rc1
Gem rails-6.0.0.rc1
actioncable (= 6.0.0.rc1)
actionmailbox (= 6.0.0.rc1)
actionmailer (= 6.0.0.rc1)
actionpack (= 6.0.0.rc1)
actiontext (= 6.0.0.rc1)
actionview (= 6.0.0.rc1)
activejob (= 6.0.0.rc1)
activemodel (= 6.0.0.rc1)
activerecord (= 6.0.0.rc1)
activestorage (= 6.0.0.rc1)
activesupport (= 6.0.0.rc1)
bundler (>= 1.3.0)
railties (= 6.0.0.rc1)
sprockets-rails (>= 2.0.0)

The core of this is railties.

Quick reminder:

  • Rails::Railtie is the core of the Rails framework. It provides a set of hooks (such as after_initialize, add_routing_paths or set_load_path) to extend Rails and/or modify the initialization process.
  • A railtie is a subclass of Rails::Railtie that's going to extend Rails. It uses the hooks provided by Railties to plug itself to Rails. Said differently, it's not Rails that knows of other components beforehand and requires them but rather the components that each implement a railtie and include themselves into Rails, letting Rails know that they're here.
  • An engine is a railtie with some initializers already set.
  • Rails::Application is an engine.

If you want to learn more about this, there’s no better way than to read the source code:

$ cd `bundle show railties`
$ ls
$ # have fun

Back to ./bin/rails. What's in it?

The part about Spring is out of the scope of this blog so we’re just going to skip it. In case you don’t know what it is:

Spring is a Rails application preloader. It speeds up development by keeping your application running in the background so you don’t need to boot it every time you run a test, rake task or migration.
APP_PATH = File.expand_path('../config/application', __dir__)

This finds the absolute path to ./config/application.rb which defines Iluvrails::Application (which inherits from Rails::Application).

This is when we start connecting the dots: your Rails application is a railtie. It doesn’t just include Rails: it plugs itself to Rails.

The last two instructions of ./bin/rails are:

require_relative '../config/boot'
require 'rails/commands'

We’re going to look at each one of them.

First, we set ENV['BUNDLE_GEMFILE'] (if not already set) to the absolute path of our Gemfile. This is for bundler to load the required gems later on. Then we require bundler/setup and bootsnap/setup.

bundler/setup checks your ruby version, which plateform you're running, ensures your Gemfile and Gemfile.lock match, etc. It basically does preliminary checks but does not require gems yet.

Bootsnap is a tool that helps speed up the boot time of an app thanks to caching operations. As for Spring, this is out of the scope of this blog so I’m skipping that part.

At this point, Rails is going to run the command you asked it to run: server. Here's an overview of how it goes.

Rails will require rails/command then run Rails::Command.invoke command, ARGV which will end up calling Rails::Command::ServerCommand.perform. Take a look at its code:

What’s most interesting for us here is that it:

  • creates a new instance of Rails::Server which is a subclass of Rack::Server
  • requires APP_PATH, which points to our ./config/application.rb
  • changes current directory to Rails.application.root
  • then basically calls #start on the Rails::Server instance.
Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.

At this point, if everything went well, the application boots and you can access it from your web browser.

That’s it. Thank you for reading!

Humm.. not so fast. Haven’t we read some require APP_PATH statement? Well, let's see what happens there.

First there’s require_relative 'boot'. We've already required this file, so at this point nothing happens. Then we require 'rails/all'.

This requires each Rails component’s railtie. Now you know how they all get included in your application.

When you don’t need all of them, you can lighten your application by removing this I want everything require statement and manually requiring only the ones you need.

Let’s say you don’t want test_unit to be included. You would replace require 'rails/all' by something like this:

require "rails"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_view/railtie"
require "action_mailer/railtie"
require "active_job/railtie"
require "action_cable/engine"
require "action_mailbox/engine"
require "action_text/engine"
# require "rails/test_unit/railtie"
require "sprockets/railtie"

Once you required rails components, it’s time for your application’s gems to be required using bundler:

Bundler.require(*Rails.groups)

Then we define our Application class:

Okay, so we defined a railtie, but there are still a missing part in the puzzle. When are initializers and ./config/environments/#{Rails.env}.rb loaded?

Back to Rails::Command::ServerCommand.perform, we see that Rails::Server is initialized in the following manner: Rails::Server.new(server_options) and when we look for server_options, we see that it is a hash with the following default values:

TL;DR: Rake is told to load ./config.ru

Okay, let’s follow this lead. We first load config/environment.rb:

So after requiring ./config/application.rb (which is already required at this point), #initialize! is called.

This is a big piece that would deserve a blog post on its own. Without going too much into details, let’s remember that Rails is made of many hooks. Some of them are related to initialization. During #run_initializers will be run among other hooks:

  • load_environment_config which loads ./config/environments/#{Rails.env}.rb
  • load_config_initializers which loads ./config/initializers/*.rb

The source code of Rails::Application gives us a quick reminder of how the boot process goes:

1) require “config/boot.rb” to setup load paths
2) require railties and engines
3) Define Rails.application as “class MyApp::Application < Rails::Application”
4) Run config.before_configuration callbacks
5) Load config/environments/ENV.rb
6) Run config.before_initialize callbacks
7) Run Railtie#initializer defined by railties, engines and application. One by one, each engine sets up its load paths, routes and runs its config/initializers/* files.
8) Custom Railtie#initializers added by railties, engines and applications are executed
9) Build the middleware stack and run to_prepare callbacks
10) Run config.before_eager_load and eager_load! if eager_load is true
11) Run config.after_initialize callbacks

Said differently:

  • Set APP_PATH to ./config/application.rb
  • Set ENV['BUNDLE_GEMFILE'] to ./Gemfile
  • Setup Bundler (without requiring gems yet)
  • Initialize a new Rails::Server (subclass of Rake::Server)
  • Require all Rails components (ActiveRecord, ActionPack, etc.)
  • Require all gems from your Gemfile
  • Define an Application that is a subclass of Rails::Application
  • Change directory to the root of your Rails application
  • Start the Rails::Server initialized earlier
  • Run Rails hooks in an orderly manner (load configuration, run initializers, etc.)
  • Your server is now waiting for requests!

We love Rails for all the magic it does for us but it’s better to understand how the magic works.