How To Set Up Zero Downtime Rails Deploys Using Puma and Foreman
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