We hope you find this tutorial helpful. In addition to guides like this one, we provide simple cloud infrastructure for developers. Learn more →

How To Set Up Zero Downtime Rails Deploys Using Puma and Foreman

PostedSeptember 10, 2013 63k views Ruby on Rails Ruby

Introduction

Puma is an efficient Ruby web server that handles Rack apps (such as Rails) very well. Puma offers concurrency using threads and/or workers. You can use Puma's clustered mode (using workers) to deploy apps without downtime. Puma will restart workers one by one, other workers will continue to handle during this time. This is good because your users will no longer see delayed responses or error pages when you deploy updates to your app.

In this guide, we are going to use the following:

  • Puma as our web server
  • Foreman to manage our app (this is not strictly necessary but it does make life a bit easier)
  • Capistrano to deploy (the deployment instructions are fairly generic and can easily be re-applied to other methods)

This guide assumes you have an existing Rails app you'll be using. See the Rails Getting Started guide if you're not up to there yet. This guide also assumes you are using Ubuntu; read up on how to set it up here. We will be using Upstart in this guide.

Installing Puma

Puma is installable via RubyGems:

gem install puma

Once installed, you can launch an app using Puma simply by calling:

puma config.ru # or whatever your *.ru file is called

But instead, we're going to use Foreman to manage our app. Read on.

Installing Foreman

Foreman can be installed using RubyGems:

gem install foreman

Configuring Rails with Puma and Foreman

First, add puma and foreman to your Gemfile:

# Gemfile
gem 'puma'
gem 'foreman'

And install:

bundle install

Foreman requires a Procfile which is used to specify processes to run. You can give processes names, for example web and worker, which you can then use to distinguish between which types of processes to run. The next thing we will do is create our Procfile:

echo "web: bundle exec puma -e $RAILS_ENV -p 5000 -S ~/puma -C config/puma.rb" >> Procfile

You can change the port number to anything you like, we'll use 5000. Next, create a puma config file and edit it to resemble this:

# config/puma.rb
threads 1, 6
workers 2

on_worker_boot do
  require "active_record"
  cwd = File.dirname(__FILE__)+"/.."
  ActiveRecord::Base.connection.disconnect! rescue ActiveRecord::ConnectionNotEstablished
  ActiveRecord::Base.establish_connection(ENV["DATABASE_URL"] || YAML.load_file("#{cwd}/config/database.yml")[ENV["RAILS_ENV"]])
end

It is important to configure the number of threads and workers correctly, and the Puma README offers some advice on how best to do this. To run Puma in clustered mode, you will need at least two workers. Generally you should match the number of cores available on your VPS. On Ubuntu you can use the command:

grep -c processor /proc/cpuinfo

to check how many cores you have. Alternatively, you can tell based on the type of droplet you are using. Note, also, that each worker will use the given number of threads, so with the configuration above, there will be a minimum of 2 and and a maximum of 12 threads.

The on_worker_boot block establishes ActiveRecord connections whenever a worker is booted (ie. during a deploy). If you are using a different ORM you will want to make the appropriate connections here.

Once you have configured your Procfile and puma.rb you should be able to run your application. To do this, simply run the command:

foreman start

Once loaded your app should be available at localhost:5000 (or whichever port you specified in your Procfile).

Deployments

Foreman is able to export to other process management formats; we are going to use Upstart, which is the Ubuntu process manager. You can export Ubstart-compatible scripts using Foreman by running:

sudo foreman export upstart /etc/init -a puma-test-app -u puma-test-user -l /var/puma-test-app/log

In this example, puma-test-app and puma-test-user would be replaced by the appropriate app name and system user. We don't want to do this locally, though - what we want to do is automatically create upstart scripts as part of our deployment, so that we are always launching the app correctly and letting Upstart ensure it keeps running.

We'll use Capistrano to deploy. If you haven't already done so, set up your app to work with Capistrano by running:

capify .

in your app's root directory. Next, in the file created at config/deploy.rb, add the following:

# config/deploy.rb
set :app_name, "puma-test-app"
set :user, "puma-test-user"

namespace :foreman do
  desc "Export the Procfile to Ubuntu's upstart scripts"
  task :export, :roles => :app do
    run "cd #{current_path} && #{sudo} foreman export upstart /etc/init -a #{app_name} -u #{user} -l /var/#{app_name}/log"
  end

  desc "Start the application services"
  task :start, :roles => :app do
    run "#{sudo} service #{app_name} start"
  end

  desc "Stop the application services"
  task :stop, :roles => :app do
    run "#{sudo} service #{app_name} stop"
  end

  desc "Restart the application services"
  task :restart, :roles => :app do
    run "#{sudo} service #{app_name} start || #{sudo} service #{app_name} restart"
  end
end

The foreman:export task will update Ubuntu's Upstart scripts, we will call this whenever we deploy. The other tasks manage the Upstart-backed service. Your Procfile will not change regularly, but if it does, you'll need to restart the application service to register these changes; you can do now do this by running:

cap foreman:restart

However, in the majority of cases, all we will want to do is tell Puma to do a phased restart. To do this, we are going to use the Capistrano deploy:restart task, which is run automatically as part of a standard Capistrano deployment. Add the following to your config/deploy.rb:

namespace :deploy do
  task :restart, :roles => :app do
    foreman.export

    # on OS X the equivalent pid-finding command is `ps | grep '/puma' | head -n 1 | awk {'print $1'}`
    run "(kill -s SIGUSR1 $(ps -C ruby -F | grep '/puma' | awk {'print $2'})) || #{sudo} service #{app_name} restart"

    # foreman.restart # uncomment this (and comment line above) if we need to read changes to the procfile
  end
end

The first thing the deploy:restart task does is call foreman:export to update our Upstart scripts. Then, we send the SIGUSR1 signal to all running instances of Puma. It does this by finding the process ID for the Puma master process, then sending it the appropriate signal. The commented out command finds the PID on OS X, it may be helpful to keep both on hand in case you need to test locally. When Puma receives the SIGUSR1 signal it will, commence a phased restart. If we are unable to send the SIGUSR1 signal for whatever reason, we fall back to restarting the service the old fashioned way.

If you need to register changes to your Procfile then you will have to call the foreman:restart task. To this, comment out the line that starts with run, and uncomment the foreman.restart line. You shouldn't need to do this regularly as ideally your Procfile would remain the same between deployments.

Testing

Before testing, make sure Foreman is installed on your Ubuntu VPS using the instructions above.

If you haven't already, you'll need to set up your VPS to work with Capistrano. First, fill in the blanks in your config.deploy.rb for your web server, repository, etc. Then configure your VPS by running:

cap deploy:setup

You can then run:

cap deploy

to do your first deployment. During this deploy, your app will be started as an Ubuntu service on your VPS. In all subsequent deployments, Puma will get sent the SIGUSR1 signal which will trigger a phased restart; you should be able to continue using your app throughout the restart process.

Database migrations

While Puma does a phased restart, your app will be running two different codebases. Thus, you will need to ensure that both codebases work with your existing database schema, which may be tricky if you have to make migrations. (If you have a substantial migration to make and need to take the site offline, you can do so by calling your new foreman:stop Capistrano task, then calling the foreman:start task to go back online later.)

9 Comments

Creative Commons License