A Shining Ruby in Production Environments

Fabrizio Soppelsa

Issue #238, February 2014

Rails, the popular Ruby framework, is straightforward in development but hard to manage. Read on to learn how to set up Ruby hosting and automatically deploy a Rails application.

Even the most beautiful Rails application can lose its elegance if not deployed correctly. Like other Ruby frameworks or languages, such as Sinatra, Rails is based on the Rack interface. This article provides a basic introduction to Rack hosting and Rack-based application deployments.

When Rails first was released in 2005, developers exulted. Finally, a comprehensive open-source framework for Web applications was available, packed with a set of tools making Web development fast, productive and fun. Rails has the reputation of being a “heaven for developers”, but despite the many facilities it provides for avoiding typical and repetitive tasks, there is still a weak spot: deployment. Deploying a Rails application is not a smooth matter. Everyone knows that Rails applications will be published on-line one day, but not precisely how.

Platform as a Service (PaaS)

Developers often choose to purchase hosting space as Platform as a Service (for example, Heroku, OpenShift or EngineYard). PaaS is marvelous as it provides a ready-to-use environment containing a full stack of software dependencies. Publishing on a PaaS platform is, as a rule, easy, fast and everything tends to work (almost) immediately. But there are at least two cases when PaaS won't fit your needs: when applications must be kept in the customer's private infrastructure or when applications have superior hardware or software requirements—for instance, when you need a specific software service not supported by your PaaS provider.

In such situations, you must implement custom virtual server configurations and custom deployment procedures. You can deploy Rails applications on servers or on virtual machines. The availability of entire cloud services like Amazon Web Services (AWS), which allow you to create complex infrastructures made of several Web servers, database servers and front-end balancing machines, is hugely growing in popularity. This approach is very flexible, although you must access, install and manage the operating system and the distribution packages, configure the network, activate the services, and so on and so forth. In this article, I describe the Rack-based hosting software requirements and some basic example configurations to implement automated Ruby hosting on a GNU/Linux server.

RVM

First, if you want to host Ruby software, you must install the Ruby platform. You can install Ruby and gems with apt-get or yum. It's easy, but when your application requires specific gem versions or specific interpreter versions, you will face a common problem. How can you satisfy these requests if your GNU/Linux distribution doesn't package those specific versions? Furthermore, how can you maintain multiple Ruby versions in a clean and repeatable manner?

You may think you can just download the Ruby platform and compile it manually. It's guaranteed that you can install the interpreter versions and the gem versions you need. Unfortunately, this is totally inconvenient. This kind of software management makes your configuration hard to update.

There are several solutions for overcoming these common issues. The one I find more reliable for server environments is named Ruby enVironment Manager (RVM). RVM comes packed with a set of scripts that helps you install and update the Ruby ecosystem.

Download RVM by issuing the following command as root:

# \curl -L https://get.rvm.io | bash -s stable

Despite the fact that it's recommended that you work with RVM using security facilities as sudo, the rvm executable must be available in your root $PATH environment, so install it as root. For a multiuser RVM installation, typical for servers, the software is kept by default in the /usr/local/rvm directory, so you can remove the whole distribution safely with an rm -fr /usr/local/rvm command.

Before proceeding with the Ruby installation, make sure your system is ready to compile Ruby. Check that you have the rvm command available in your PATH (if not, log out and log in again or reload your shell with bash -s), and execute the following command:

$ sudo rvm requirements

RVM will install, through yum or apt-get, the required packages to compile the Ruby distribution. In this article, I use the stable official Ruby distribution called MRI, Matz Ruby Interpreter (derived from the name of Ruby's creator, Yukihiro Matsumoto).

Now, you'll likely need to add to your future Rubies some basic libraries typically needed by some complex gems or software. Setting up such libraries immediately will guarantee that the Ruby software will never complain that the system libraries are old or incompatible, generating annoying errors. Previously, you would have installed these extra packages via the rvm pkg install <pkg> command, but now RVM deprecates this. Instead, simply enable autolibs to delegate to RVM the responsibility to build coherent and not-buggy distributions:

$ sudo rvm autolibs enable

You finally are ready to provide your environment a full Ruby distribution. For example, let's install the latest stable version of the official MRI interpreter, the 2.0.0 version:

$ sudo rvm install 2.0.0

If everything goes well, the distribution is available for root and the system users. If not, it's commonly a $PATH problem, so adjust it in the /etc/profile.d, and also to avoid deployment pitfalls, verify that the $GEM_HOME variable is exported to the correct gem path. In practice, if something is not working properly, set the following variables like this:

if [ -d "/usr/local/rvm/bin" ] ; then
    PATH="/usr/local/rvm/gems/ruby-2.0.0-p353@global/bin:
↪/usr/local/rvm/bin:$PATH"
    GEM_HOME="/usr/local/rvm/gems/ruby-2.0.0-p353@global"
fi

You can list the available Ruby versions with this command:

$ rvm list known

On a system running multiple Rubies, users and system processes may load other environment versions with a command like this:

$ rvm use jruby-1.7.1

And set the default system distribution in this way:

$ rvm --default use 2.0

The Web Server

Ruby on Rails, like Sinatra and many other popular Ruby frameworks or Domain Specific Languages, is based on an interface named Rack. Rack provides the minimal abstraction possible between Web servers supporting Ruby and Ruby frameworks. Rack is responsible for invoking the main instance of your application as specified in the startup file, config.ru.

So, a Web server hosting Ruby Web applications will have to understand how Rack talks. With a stable and clean Ruby environment, you're ready to build your Web server that is capable of speaking Rack.

With Ruby, you can choose between many Web servers. You may have heard of Mongrel, Unicorn, Thin, Reel or Goliath. For typical Rails deployments, Passenger is one of the most popular choices. It integrates well with Apache and Nginx, so in this example, let's set up an Apache + Passenger configuration.

Passenger Installation

Passenger, developed by Phusion, also formerly known as mod_rails or mod_rack, is a module that allows you to publish Ruby applications in the popular Web server containers Apache or Nginx. Passenger is available as a “community” free edition and as an enterprise release, which includes commercial support and advanced features.

If you chose to install Ruby through packages, Passenger is conveniently available through RPM or DEB repositories, and yum or apt-get will install all the required software.

On an RVM-customized system, to install the free version of Passenger, you need to add the gem through Ruby gems:

$ sudo gem install passenger

Now you can install the server module (the latest version at the time of this writing is 4.0.33) by executing a script provided by the gem:

# passenger-install-apache2-module

Let's select Ruby only, and let's skip Python, Node.js and Meteor support. If your system misses software requirements, the script will give you a tip to the exact command line for yum or apt-get to meet those dependencies.

After some compile time, you will be introduced to Passenger configuration with useful and self-explanatory output. Specifically, copy to the directives that load Passenger into Apache in your main Apache configuration file (apache2.conf or httpd.conf):

LoadModule passenger_module
/usr/local/rvm/gems/ruby-2.0.0-p353/gems/passenger-4.0.33/
↪buildout/apache2/mod_passenger.so
PassengerRoot /usr/local/rvm/gems/ruby-2.0.0-p353/gems/
↪passenger-4.0.33
PassengerDefaultRuby /usr/local/rvm/wrappers/ruby-2.0.0-p353/ruby

Finally, restart Apache. Et voilà, now you can host Ruby Web applications.

Virtual Hosts

If your goal is to host one or more Ruby applications on the same server, you should activate each instance as a virtual host. The most significant directive with Ruby hosting is the DocumentRoot. It's mandatory that it points to the public/ directory in the application's root project directory. The public/ directory is the default public path of a Rails application. So let's say you have a Kolobok application made in Rails, and you have to deploy it to the DNS zone kolobok.example.com on the kolobok.example.com server. Here is an example VirtualHost:


<VirtualHost *:80>
      ServerName kolobok.example.com
      DocumentRoot /srv/www/kolobok/public
      <Directory /srv/www/kolobok/public>
         # This relaxes Apache security settings.
         AllowOverride all
         # MultiViews must be turned off.
         Options -MultiViews
      </Directory>
</VirtualHost>

Now, if you have put your application in /srv/www/kolobok, and it's well configured (configured and binded to the database and so on), enable the virtual host, reload Apache, and your application is published.

Automating Software Deployments

Ages ago, it was common to deploy Web applications by doing a bulk copy of files via FTP, from the developer's desktop to the server hosting space, or by downloading through Subversion or Git. Although this approach still works for simpler PHP applications, it won't fit more complex projects made using more complex frameworks, such as Rails.

In fact, a Rails application is not made only of the source code files. To make a Rails application ready, you have to download and compile its dependencies as gems (by running bundle), safely manage database access and other configurations, migrate the database (create the database and the schema by executing a list of files containing SQL instructions in the Ruby language), adjust paths for shared content (like images, videos and so on), precompile the assets (that is, optimizing static content, such as JavaScript and CSS), and perform many other steps in a large and complex work flow. You can execute these steps by writing your own scripts, maybe in Ruby or bash, but this task is tedious and wastes your time. You should instead invest your time by writing good tests.

The Ruby community provides several ways to accomplish the whole deploy task, and one very popular method uses Capistrano. Capistrano lets you write a set of “recipes” that will “cook” your application in the production environment. Common tasks executed by Capistrano are: 1) pulling the source code from a git or svn repository; 2) putting it in the right location; 3) checking if a bundle is needed and, if yes, bundling your gems; 4) checking if migrations are required and, if yes, running them; 5) checking if assets precompile is required and, if yes, precompiling; and 6) checking other Rake tasks you have defined and running them in order. If the whole recipe fails, Capistrano will keep the current software release in production; otherwise, it will substitute the latest release with the one you've just deployed. Capistrano is a largely tested and very reliable tool. You definitely can trust it.

Configuring Capistrano

To use Capistrano, you just need to install it through Ruby gems on the system where the deploy will be done (not on the server):

$ gem install capistrano

When Capistrano is available, you'll have two new binaries in your PATH: capify and cap. With capify, you build your deploy skeleton. So, cd to the project directory and type:

$ capify .

This command creates a file named Capfile and a config/deploy.rb file. Capfile tells Capistrano where the right deploy.rb configuration file is. This is the file that includes your recipes, and typically it's kept in the project's config/ directory.

Next, verify that Capistrano is installed correctly, and see the many useful tasks it comes with:

$ cap -T
cap deploy                # Deploys your project.
cap deploy:check          # Tests deployment dependencies.
cap deploy:cleanup        # Cleans up old releases.
cap deploy:cold           # Deploys and starts a 'cold' application.
cap deploy:create_symlink # Updates the symlink to the most recently
                          # deployed...
cap deploy:migrations     # Deploys and runs pending migrations.
cap deploy:pending        # Displays the commits since your last 
                          # deploy.
cap deploy:pending:diff   # Displays the 'diff' since your last 
                          # deploy.
cap deploy:rollback       # Rolls back to a previous version and 
                          # restarts.
cap deploy:rollback:code  # Rolls back to the previously deployed 
                          # version.
cap deploy:setup          # Prepares one or more servers for 
                          # deployment.
cap deploy:symlink        # Deprecated API.
cap deploy:update         # Copies your project and updates the 
                          # symlink.
cap deploy:update_code    # Copies your project to the remote 
                          # servers.
cap deploy:upload         # Copies files to the currently deployed 
                          # version.
cap invoke                # Invokes a single command on the remote 
                          # servers.
cap link_shared           # Link cake, configuration, themes, upload, 
                          # tool
cap shell                 # Begins an interactive Capistrano session.

The user that will deploy the application will need valid SSH access to the server (in order to perform remote commands with Capistrano) and write permissions to the directory where the project will be deployed. The directory structure created on the server in this directory allows you to maintain software releases. In the project's document root, Capistrano keeps two directories, one that contains the released software (releases/, by default it keeps the latest ten releases), and another that contains shared or static data (shared/). Moreover, Capistrano manages a symbolic link named current that always points to the most recent successfully deployed release.

In practice, each time Capistrano is invoked to deploy an application, it connects via SSH, creates a temporary release directory named with the current timestamp (for example, releases/20140115120050), and runs the process (pull, bundle, migrate and so on). If it finishes with no errors, as final step, Capistrano links the symlink “current” to releases/20140115120050. Otherwise, it keeps “current” symlinked with the latest directory where the deploy was successful.

So with Capistrano, the system administrator will set the virtual server DocumentRoot directive to the current directory of the released application version:

DocumentRoot /srv/www/kolobok/current/public

The Anatomy of a deploy.rb File

A deploy.rb file is virtually made of two parts: one that defines the standard configurations, like the repository server or the path to deploy files physically, and another that includes custom tasks defined by the developer responsible for deploying the application.

Let's deploy the Kolobok application. Open the kolobok/config/deploy.rb file with your favourite editor, delete the example configuration and begin to code it from scratch. A deploy.rb file is programmed in Ruby, so you can use Ruby constructs in your tasks, beyond the Capistrano “keywords”.

Let's start by requiring a library:

require "bundler/capistrano"

This statement orders Capistrano to do the gem bundle each time it's necessary. Good gem files separate required dependency gems in this way:

group :test do
  gem 'rspec-rails'
  gem 'capybara'
  gem 'factory_girl_rails'
end


group :production do    
  gem 'execjs'
  gem 'therubyracer'  
  gem 'coffee-rails', '~> 3.1.1'
end

Only the gems common to all environments and included in the :production group are bundled. Gems belonging to :development and :test environments are not. And the first time you deploy your application, a bundle install is executed to bundle all the requirements as specified. The next time you deploy the software, gems are downloaded, compiled or removed only if the Gemfile and the Gemfile.lock have changed. The complete bundle is installed in shared/ and soft-linked into the current instance. By following this approach, less disk space is required.

Then, from Rails 3.1, it's common to release applications with the assets pipeline. The pipeline is active if in config/environments/production.rb the following variable is set to true:

config.assets.compile = true

If your application will use the pipeline, you need to precompile it. The Rake task to precompile assets is bundle exec rake assets:precompile. To insert this task into your work flow and keep the generated assets pipeline in shared/ and linked into the current release, load the standard assets functionality:

load "deploy/assets"

After loading the main requirements, specify the application name, the path on the server where it will be deployed, and the user allowed to SSH:

set :application, "kolobok"
set :deploy_to, "/srv/www/kolobok" 
et :user, "myuser"

With Rails > 3, it's recommended to invoke Rake (it's used to do the database migrations and to precompile the assets pipeline) with the correct bundled Rake version in the bundle. So, specify the exact rake command:

set :rake, 'bundle exec rake'

Now it's time to configure the repository from which to pull the project source code:

set :scm, :git
set :branch, "master"
set :repository, "git://github.com/myusername/kolobok.git"

Finally, set the server names:

role :web, "kolobok.example.com"
role :app, "kolobok.example.com"
role :db,  "mydb.example.com", :primary => true 

web is the address of the responding Web server, and app is where the application will be deployed. These roles are the same if the application runs on only one host rather than on a cluster. db is the address of the database, and primary => true means that migrations will be run there.

Now you have a well-defined deploy.rb and the right server configurations. Begin by creating the structure tree (releases/ and static/) on the server, from the desktop host:

$ cap deploy:setup

Releasing Software

After having set up the project directory on the server, run the first deploy:

$ cap deploy:cold

The actions performed by Capistrano follow the standard pattern: git checkout, bundle, execute migrations, assets precompile. If everything is fine, your application is finally published as a reliable versioned release, with a current symlink.

Normal deploys (skipping the first Rails app configuration, such as creating the database) will be done in the future by invoking:

$ cap deploy

If you notice that some errors occurred with the current application in production, you immediately can roll back to the previous release by calling Capistrano like this:

$ cap deploy:rollback

Easy, reliable and smart, isn't it?

Custom Tasks

When you deploy a more complex application, you'll normally be handling more complex recipes than the standard Capistrano procedure. For example, if you want to publish an application on GitHub and release it open source, you won't put configurations there (like credentials to access databases or session secret tokens). Rather, it's preferable to copy them in shared/ on the server and link them on the fly before modifying the database or performing your tasks.

In Capistrano, you can define hooks to actions to force the tool to execute required actions before or after other actions. It might be useful, for instance, to link a directory where users of kolobok have uploaded files. If you move the current directory to another release path, you might discover that those files are no longer available to users. So, you can define a final task that, after having deployed code, links the shared/uploads into your current release in public/uploads directory. Notice how this can be managed with ease by exploiting the presence of the shared_path and release_path paths variables:

desc "Link uploaded directory"
task :link_uploads do
  run "ln -nfs #{shared_path}/uploads 
   ↪#{release_path}/public/uploads"
end

Finally, another common task to perform is to restart the application instance into the server container. In case of Passenger, it's enough to touch the tmp/restart.txt file. So, you can write:


desc "Restart Passenger" 
task :restart do
  run "cd #{current_path} && touch tmp/restart.txt" 
end

You execute these two tasks automatically by hooking them at the end of the deploy flow. So add this extra line just before the tasks definitions:

after "deploy:update_code", :link_uploads, :restart

Performance Issues?

People often complain of Rails' performance in production environments. This is a tricky topic. Tuning servers and application responsiveness are rather hard tasks that cannot be discussed briefly, so I don't cover them here. To make your application faster, you should involve several technologies and engineering patterns, like setting intermediate caching services, serving static and dynamic content with different server containers and monitoring the application with tools like New Relic to find bottlenecks. After having set up the right environment to host the application, this is the next challenge—optimizing. Happy deploys!

Fabrizio Soppelsa works as a sysadmin for an Italian provider. His areas of interest include Ruby hosting, automated Ruby deployments, Ruby application performance in production environments, Platform as a Service, and scalable and event-driven Web patterns. He is passionate about espresso coffee and lives in Italy with his cat. He is a Red Hat-certified engineer, and you can follow him on Twitter: @f_soppelsa.