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
./bin/rails
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 asafter_initialize
,add_routing_paths
orset_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?
#!/usr/bin/env ruby
begin
load File.expand_path('../spring', __FILE__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
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.
./config/boot.rb
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
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.
rails/commands
At this point, Rails is going to run the command you asked it to run: server
. Here's an overview of how it goes.
# frozen_string_literal: true
require "rails/command"
aliases = {
"g" => "generate",
"d" => "destroy",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner",
"t" => "test"
}
command = ARGV.shift
command = aliases[command] || command
Rails::Command.invoke command, ARGV
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:
def perform
extract_environment_option_from_argument
set_application_directory!
prepare_restart
Rails::Server.new(server_options).tap do |server|
# Require application after server sets environment to propagate
# the --environment option.
require APP_PATH
Dir.chdir(Rails.application.root)
if server.serveable?
print_boot_information(server.server, server.served_url)
after_stop_callback = -> { say "Exiting" unless options[:daemon] }
server.start(after_stop_callback)
else
say rack_server_suggestion(using)
end
end
end
What’s most interesting for us here is that it:
creates a new instance of
Rails::Server
which is a subclass ofRack::Server
requires
APP_PATH
, which points to our./config/application.rb
changes current directory to
Rails.application.root
then basically calls
#start
on theRails::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.
./config/application.rb
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 Iluvrails
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
end
end
First there’s require_relative 'boot'
. We've already required this file, so at this point nothing happens. Then we require 'rails/all'
.
# frozen_string_literal: true
# rubocop:disable Style/RedundantBegin
require "rails"
%w(
active_record/railtie
active_storage/engine
action_controller/railtie
action_view/railtie
action_mailer/railtie
active_job/railtie
action_cable/engine
action_mailbox/engine
action_text/engine
rails/test_unit/railtie
sprockets/railtie
).each do |railtie|
begin
require railtie
rescue LoadError
end
end
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:
module Iluvrails
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
end
end
Okay, so we defined a railtie, but there still is 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:
class_option :config, aliases: "-c", type: :string, default: "config.ru",
desc: "Uses a custom rackup configuration.", banner: :file
# [...]
def server_options
{
user_supplied_options: user_supplied_options,
server: using,
log_stdout: log_to_stdout?,
Port: port,
Host: host,
DoNotReverseLookup: true,
config: options[:config],
environment: environment,
daemonize: options[:daemon],
pid: pid,
caching: options[:dev_caching],
restart_cmd: restart_command,
early_hints: early_hints
}
end
TL;DR: Rake is told to load ./config.ru
./config.ru
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.application
Okay, let’s follow this lead. We first load config/environment.rb
:
# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!
So after requiring ./config/application.rb
(which is already required at this point), #initialize!
is called.
# Initialize the application passing the given group. By default, the
# group is :default
def initialize!(group = :default) #:nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@initialized = true
self
end
def run_initializers(group = :default, *args)
return if instance_variable_defined?(:@ran)
initializers.tsort_each do |initializer|
initializer.run(*args) if initializer.belongs_to?(group)
end
@ran = true
end
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
Closing note
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 ofRack::Server
)Require all Rails components (ActiveRecord, ActionPack, etc.)
Require all gems from your Gemfile
Define an
Application
that is a subclass ofRails::Application
Change directory to the root of your Rails application
Start the
Rails::Server
initialized earlierRun 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.