Pothibo

Dissecting rails server command

How do you create a new project in Ruby on Rails?

rails new my_new_project

Easy peasy japanesey.

Of all the folders and file that rails generate for you, how much of it is necessary?

How does Rails go from rails server to loading your project?

Knowing this stuff is not necessary for you to develop your project. Rails is a huge framework and knowing everything about it is almost impossible. However, it's refreshing to try to understand things that you may take for granted. Learning is a process, and creativity comes by understanding what other people do.

Even though you probably have an idea of how the boot up works, I'm sure you'll find things that you didn't expect. I surely did!

Ready? Let's get started!

Create the project

First create an empty folder. I'll use the name rails-boot.

How it starts

When we call rails server, Rails loads Rails::AppRailsLoader through Rails::CLI.

The CLI class doesn't do much. On the other hand, AppRailsLoader does some interesting stuff.

# https://github.com/rails/rails/blob/4-1-stable/railties/lib/rails/app_rails_loader.rb

...

original_cwd = Dir.pwd

loop do

if exe = find_executable

contents = File.read(exe)

if contents =~ /(APP|ENGINE)_PATH/

exec RUBY, exe, *ARGV

break # non reachable, hack to be able to stub exec in the test suite

elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')

$stderr.puts(BUNDLER_WARNING)

Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))

require File.expand_path('../boot', APP_PATH)

require 'rails/commands'

break

end

end

Dir.chdir('..')

end

...

This piece of code starts by iterating (#5) from the current working directory to the root of the filesystem (#20) by looking at either bin/rails or script/rails (#6). If it finds an occurrence of one of these files, it will execute it and break the loop (#9-12).

In order for the binary to be valid, it needs to either have APP_PATH or ENGINE_PATH present in the file.

Now that we know what we need, let's start building our project.

# rails-boot/bin/rails

APP_PATH = "dunno what goes here, yet."

If we run it, it will run and return. Nothing is happening.

We could give up, but this post wouldn't make much sense now, would it? Let's look at what Rails does by default.

# https://github.com/rails/rails/blob/master/railties/lib/rails/generators/rails/app/templates/bin/rails

APP_PATH = File.expand_path('../../config/application', __FILE__)

require_relative '../config/boot'

require 'rails/commands'

Now it starts to make sense. It loads the application class, then boots it and finishes by loading the command we requested — rails server.

The booting process is simply loading the Gemfile through bundler. For the sake of this example, I'll skip bundler and get down to loading the application.

# https://github.com/rails/rails/blob/master/railties/lib/rails/generators/rails/app/templates/bin/rails

APP_PATH = File.expand_path('../../application', __FILE__)

require 'rails/commands'

The Application class

Any rails application has a file called application.rb. That file usually lives in config/ and has some default settings in it. Two things need to be set in this file for it to work.

First, we need to require rails. Up until now, the project we're building doesn't even have a rails environment.

Second, we need to create an arbitrary module that will be the name of our project — RailsBoot. Inside that module, an application class needs to be defined as well.

# rails-boot/application.rb

require 'rails/all'

module RailsBoot

class Application < Rails::Application

end

end

If we try to run a development server off this project, we will see that Rails actually boot and then crash.

% rails server

=> Booting Thin

=> Rails 4.1.2.rc1 application starting in development on http://0.0.0.0:3000

=> Run `rails server -h` for more startup options

=> Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option)

=> Ctrl-C to shutdown server

configuration config.ru not found

Exiting

Error message is clear enough: config.ru is missing.

That file is necessary for any rack-based application. It comes as no surprise that this file is mandatory to have Rails working. In essence, that file need to have a single line of code: run(application).

The application needs to be rack compliant. RailsBoot::Application is.

Since application.rb is already loaded when config.ru is reached, it's possible to initialize the application right away. Here's how it should look like.

# rails-boot/config.ru

run RailsBoot::Application.initialize!

Now, if we run rails server, Rails should start the server without crashing!

... until we try to reach the server on the browser.

Runtime configurations

Now that the server can run, it's time to look at the runtime configurations that Rails needs to start working as expected.

First, if we tried to reach the homepage, this error would come up:

Unexpected error while processing request: Missing `secret_key_base`

for 'development' environment, set this value in `config/secrets.yml`

Even if this is self-explanatory, here's how config/secrets.yml should look like.

# rails-boot/config/secrets.yml

defaults: &defaults

secret_key_base: "Some long random key in here"

development:

<<: *defaults

Reloading the server and accessing it via the browser will lead to another error.

RuntimeError (Cannot load `Rails.application.database_configuration`:

Could not load database configuration. No such file - ):

While this error is a little bit ambiguous without a file name, we know that the database configuration file is config/database.yml. Now, I would like to merge this configuration file with config/secrets.yml as they share a lot of similarities: they are both YAML files, both have the same environment structures (development, production, test) and finally, they don't have overlapping key.

To do that, we'll need to modify the paths in RailsBoot::Application. It's not usual, but it's quite trivial once you know how to do it. Just add the following to application.rb.

# rails-boot/application.rb

require 'rails/all'

module RailsBoot

class Application < Rails::Application

paths.add 'config/database', with: 'config/secrets.yml'

end

end

Finally, we need to add the database informations to secrets.yml.

# rails-boot/config/secrets.yml

defaults: &defaults

secret_key_base: "Some long random key in here"

adapter: 'postgresql'

development:

<<: *defaults

database: 'rails-boot'

And if we restart the application, we'll have yet another error.

ActiveRecord::NoDatabaseError (FATAL:  database "rails-boot" does not exist

Run `$ bin/rake db:create db:migrate` to create your database)

Rake requires Rakefile. That means if we want to create a database for this project, we have to create a new file: rails-boot/Rakefile.

# rails-boot/Rakefile

require File.expand_path('../application', __FILE__)

RailsBoot::Application.load_tasks

The Rakefile first loads the application. Once it's loaded, it will load the default tasks that Rails ships with. From this point, we should be able to create the database by typing in the terminal rake db:create.

From this point, Rails has all the runtime configurations it needs to run. Running the server now will result in a blank page, not the welcome page we usually get when starting a new project.

This is happening because we don't have a config/routes.rb. I really hate silent errors, but hey, this isn't an usual process so it had to be expected!

# rails-boot/config/routes.rb

RailsBoot::Application.routes.draw do

end

If we restart the server and access it with the browser, it will land on the default welcome page! We have reached our goal.

Now, I understand that it's much easier to just type rails new rails-boot and have everything created for you. Sometimes, it's refreshing to understand how things work. It gives you a new perspective on stuff you took for granted.

Figuring this whole process out made me realize that Rails was flexible enough to build very special things. If I didn't have figure this out, Ecrire would have never become a standalone gem.

Stepping stones...

Get more ideas like this to your inbox

You will never receive spam, ever.