Rails Initialization - What happens when you run Rails Server command
What happens when we run the rails server
command? A lot. It initializes the entire Rails framework, boots the application, and starts the web server. In this post, we go over the timeline of what happens and why it’s useful to understand this in practice.
Here’s a sketch of the timeline:
$ bin/rails server
↓
bin/rails script → railties/lib/rails/commands/server/server_command.rb
↓
Load config.ru
↓
require config/environment.rb
↓
require config/application.rb
↓
Rails.application.initialize!
↓
Framework + app code loaded
↓
Puma (web server) starts listening
↓
Incoming HTTP request
↓
Puma builds Rack env, calls Rails.application.call(env)
↓
Middleware stack → Router → Controller → Response
↓
Response back to Puma → client
In a previous post we focused on the bottom 4 things in the sequence above. In this post, we’ll zoom in on the middle part - environment.rb
–> applicaiton.rb
–> and all the things that happen when Rails.application.initalize!
is called. (The rails server
command also wraps the Puma web server, we won’t cover Puma much in this post).
Understanding the Rails initialization process will allow us to know:
- When different parts of the framework are available to use. (e.g.
Rails.logger
,Rails.env
,Rails.root
, etc.). - The order in which things are run so that we can debug sequencing issues (e.g. An initializer having a dependency on another initializer).
- How to customize and “hook into” the initialization process with load hooks and initialization hooks.
The environment.rb
File as the Entry Point
The config/environment.rb
file is the starting point of the Rails initialization process. You can boot the Rails framework by adding require config/environment.rb
in a file (this is what config.ru
does). The environment.rb
file is generated in all new Rails applications but you rarely need to open it:
# config/environment.rb
# Load the Rails application.
require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!
The first line requires config/application.rb
, this is the familiar file that defines our application that we often modify:
# config/application.rb
require_relative "boot"
require "rails/all"
Bundler.require(*Rails.groups)
module MyAmazingApp
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 8.0
...
end
end
Loading application.rb
loads all the Gems in our application’s Gemfile
. With the require "rails/all"
line, it loads all Railties and Engines that register initializers for all framework components such as Active Record, Active Job, etc.
Then, subclassing our Application
class from Rails::Application
causes an inherited
callback to run, which sets up Rails.root
among other helpers. Therefore Rails.root
is available in the body of the MyAmazingApp::Application
class onwards, but not before the subclassing.
The second line of the environment.rb
file above calls the initialize!
method, which kicks off setting up the entire Rails framework and booting everything. Let’s see that in more detail.
Rails.application.initalize!
When initialize!
is called, it runs all registered initializers in order, including the ones from Railties mentioned above. The initialize!
method is defined in the Rails::Application
class:
# railties/lib/rails/application.rb
# Initialize the application passing the given group.
def initialize!(group = :default) # :nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@initialized = true
self
end
...
def initializers # :nodoc:
Bootstrap.initializers_for(self) +
railties_initializers(super) +
Finisher.initializers_for(self)
end
# railties/lib/rails/initializable.rb
def run_initializers(group = :default, *args)
initializers.tsort_each do |initializer|
initializer.run(*args) if initializer.belongs_to?(group)
end
end
All groups of initializers, Bootstrap
, Railtie, and Finisher
, are run in topological order (tsort
comes from Ruby and it respects dependencies among tasks with :before
and :after
rules).
The Bootstrap
initializers prepare the application
(like initializing Rails.logger
).
The custom initializers in our application under config/initializers
are also run as part of the initialize!
call. They are registered via load_config_initializer
in Rails::Engine
Railtie. They are run in alphabetical order by file names in that directory.
The Finisher
initializers set up the middleware stack and the router and are run last.
At this point, the Rails initialization process is complete and the application is booted and ready. Now the web server can be started to accept incoming requests.
Try This
Here is a little experiment you can try to demonstrate initialization hooks and when framework components are available:
This is an example of an config.after_routes_loaded
initialization hook. Add this to your config/application.rb
:
module MyAwesomeApp
class Application < Rails::Application
# Fires very early
config.before_initialize do
puts "[before_initialize] Rails.application.routes.routes.any? #{Rails.application.routes.routes.any?}"
end
# Fires after frameworks & initializers, but BEFORE routes are loaded
config.after_initialize do
puts "[after_initialize] Rails.application.routes.routes.any? #{Rails.application.routes.routes.any?}"
end
# Fires AFTER routes are fully loaded from config/routes.rb
config.after_routes_loaded do
puts "[after_routes_loaded] Rails.application.routes.routes.any? #{Rails.application.routes.routes.any?}"
puts "[after_routes_loaded] Number of routes: #{Rails.application.routes.routes.size}"
end
...
end
Then start the Rails server and you will see that routes are not loaded until later in the initialization process:
$ bin/rails server
=> Booting Puma
=> Rails 7.1.3 application starting in development
=> Run `bin/rails server --help` for more startup options
[before_initialize] Rails.application.routes.routes.any? false
[after_initialize] Rails.application.routes.routes.any? false
[after_routes_loaded] Rails.application.routes.routes.any? true
[after_routes_loaded] Number of routes: 45
We’ll wrap it up here. You can follow how a Rails application accepts incoming HTTP requests once a web server is up and running in the previous post.
Subscribe for more Ruby, Rails, and Hotwire
Short posts, clear explanations