Deploying a Rails app on Nginx/Puma with Capistrano

Puma is a fast multi-threaded Ruby app server designed to host rack-based Ruby web apps including Sinatra and Ruby on Rails. Like Unicorn, it supports rolling restarts, but since it is multi-threaded rather than Unicorn’s multi-process model, it takes far less memory while being comparable in performance. Puma can run on Ruby 1.9.X but its multi-threaded nature is better suited to run on a real multi-threaded runtime like Rubinius or JRuby.

This article will guide you to setting up a hello world Rails app with Puma/Nginx and deploy it with Capistrano onto a linux system. This guide was tested on Puma 1.6.3 and Puma 2.0.1.

Create a base Rails app

rails new appname

Adding Puma to Rails app

We’ll start with adding a puma to your Rails app.

In your Gemfile, add:

gem "puma"

then run bundle install

Now we need a puma config file: config/puma.rb

rails_env = ENV['RAILS_ENV'] || 'development'

threads 4,4

bind  "unix:///data/apps/appname/shared/tmp/puma/appname-puma.sock"
pidfile "/data/apps/appname/current/tmp/puma/pid"
state_path "/data/apps/appname/current/tmp/puma/state"

activate_control_app

Setup Nginx with Puma

Follow the instructions to install Nginx from source. It will install nginx to /usr/local/nginx

Edit your /usr/local/nginx/conf/nginx.conf file to be below:

user deploy;
worker_processes  1;

error_log  /var/log/nginx/error.log;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /usr/local/nginx/conf/mime.types;
    default_type  application/octet-stream;

    access_log  /var/log/nginx/access.log;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;
    tcp_nodelay        on;

    gzip  on;

    server_names_hash_bucket_size 128;
    
    client_max_body_size 4M; 
    client_body_buffer_size 128k;
    
    include /usr/local/nginx/conf/conf.d/*.conf;
    include /usr/local/nginx/conf/sites-enabled/*;
}

Create a file named "puma_app" in the sites-enabled directory:

upstream appname {
  server unix:///data/apps/appname/shared/tmp/puma/appname-puma.sock;
}

server {
  listen 80;
  server_name www.appname.com appname.com;

  keepalive_timeout 5;

  root /data/apps/appname/public;

  access_log /data/log/nginx/nginx.access.log;
  error_log /data/log/nginx/nginx.error.log info;

  if (-f $document_root/maintenance.html) {
    rewrite  ^(.*)$  /maintenance.html last;
    break;
  }

  location ~ ^/(assets)/  {
    root /data/apps/appname/current/public;
    expires max;
    add_header  Cache-Control public;
  }

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    if (-f $request_filename) {
      break;
    }

    if (-f $request_filename/index.html) {
      rewrite (.*) $1/index.html break;
    }

    if (-f $request_filename.html) {
      rewrite (.*) $1.html break;
    }

    if (!-f $request_filename) {
      proxy_pass http://appname;
      break;
    }
  }

  # Now this supposedly should work as it gets the filenames 
  # with querystrings that Rails provides.
  # BUT there's a chance it could break the ajax calls.
  location ~* \.(ico|css|gif|jpe?g|png)(\?[0-9]+)?$ {
     expires max;
     break;
  }

  location ~ ^/javascripts/.*\.js(\?[0-9]+)?$ {
     expires max;
     break;
  }

  # Error pages
  # error_page 500 502 503 504 /500.html;
  location = /500.html {
    root /data/apps/appname/current/public;
  }
}

Using init scripts to start/stop/restart puma

We want to be able to start/restart puma using linux init scripts.

The init scripts for nginx should have been installed already as well. You can start nginx using `sudo /etc/init.d/nginx start

Install Jungle from Puma’s source repo. Jungle is a set of scripts to manage multiple apps running on Puma. You need the puma and run-puma files and place them into /etc/init.d/puma and /usr/local/bin/run-puma respectively.

Then, add your app config using: sudo /etc/init.d/puma add /data/apps/appname/current deploy

IMPORTANT: The init script comes with an assumption that your puma state directories live in /path/to/app/tmp/puma

Using Capistrano to deploy

In your Gemfile, add:

gem "capistrano"

then run bundle install

In your deploy.rb, change to the following below. Note the shared tmp dir modification.

#========================
#CONFIG
#========================
set :application, "APP_NAME"
set :scm, :git
set :repository, "GIT_URL"
set :branch, "master"
set :ssh_options, { :forward_agent => true }
set :stage, :production
set :user, "deploy"
set :use_sudo, false
set :runner, "deploy"
set :deploy_to, "/data/apps/#{application}"
set :app_server, :puma
set :domain, "DOMAIN_URL"
#========================
#ROLES
#========================
role :app, domain
role :web, domain
role :db, domain, :primary => true
#========================
#CUSTOM
#========================
namespace :puma do
  desc "Start Puma"
  task :start, :except => { :no_release => true } do
    run "sudo /etc/init.d/puma start #{application}"
  end
  after "deploy:start", "puma:start"

  desc "Stop Puma"
  task :stop, :except => { :no_release => true } do
    run "sudo /etc/init.d/puma stop #{application}"
  end
  after "deploy:stop", "puma:stop"

  desc "Restart Puma"
  task :restart, roles: :app do
    run "sudo /etc/init.d/puma restart #{application}"
  end
  after "deploy:restart", "puma:restart"

  desc "create a shared tmp dir for puma state files"
  task :after_symlink, roles: :app do
    run "sudo rm -rf #{release_path}/tmp"
    run "ln -s #{shared_path}/tmp #{release_path}/tmp"
  end
  after "deploy:create_symlink", "puma:after_symlink"
end

You’ll need to setup the directories one time using: cap deploy:setup

Now you can deploy your app using cap deploy and restart with cap deploy:restart.

EDIT: xijo has formalized the cap tasks as a gem, capistrano-puma to make it easier to use on multiple projects.

29 thoughts on “Deploying a Rails app on Nginx/Puma with Capistrano

    1. tommychheng Post author

      I haven’t set it up for my puma process, but I typically use monit to restart a killed process and pingdom to ensure the website is externally accessible.

      Reply
    1. tommychheng Post author

      Great to see this included but the included capistrano scripts aren’t using init.d or upstart scripts so if the server were to restart, the user would have to cap deploy:start

      Reply
    1. tommychheng Post author

      Puma definitely works best on real threaded impl like jruby but ruby 1.9.3/2.0.0 still gives Puma its ability to do IO multi-threading, e.g. serving the IO part of a web request.

      Reply
    1. tommychheng Post author

      If you want a safer bet, Unicorn has been around longer and you will be able to find solutions to related problems/configurations easier.

      Puma will use less memory while having similar performance to unicorn. It’s also newer so it hasn’t had as much adoption yet.

      With those two criteria(safe/battle-tested or more efficient/newer), i think you can make a choice.

      Reply
  1. andywenk

    Thanks a lot for your post. I have several Rails applications running with phusion-passenger 3.0.19. I thought about upgrading to 4 but on the other I thought it would be cool to use Puma. Unfortunately I can’t get it running. The *.sock file is not being created and the nginx log says (no wonder)

    2013/11/24 16:05:44 [crit] 15197#0: *96 connect() to unix:///var/www/preview.my_app/shared/tmp/puma/preview_my_app.sock failed (2: No such file or directory) while connecting to upstream

    Any idea why the sock file is not created? The capistrano log says:

    ** [out :: my_server.com] * Restarting Puma rack web server puma
    ** [out :: my_server.com] * –> Your puma was never playing… Let’s get it out there first
    ** [out :: my_server.com] * –> Woke up puma /var/www/preview_my_app/current
    ** [out :: my_server.com] * user aw
    ** [out :: my_server.com] * log to /var/www/preview_my_app/current/log/puma.log
    ** [out :: my_server.com] Starting /usr/local/bin/run-puma…
    ** [out :: my_server.com] Detaching to start /usr/local/bin/run-puma…done.

    but

    * [out :: my_server.com] * Status Puma rack web server puma
    ** [out :: my_server.com] * –> /var/www/preview_my_server/current isn’t there :(…

    Thanks a lot for your help!

    Cheers

    Andy
    P.S.: sorry for the long comment

    Reply
    1. tommychheng Post author

      does your deploy user have permissions for /var/www/preview.my_app/shared/tmp/puma/ ?

      what happens if you ssh into the machine and try to run puma manually from the app directory?

      Reply
      1. andywenk

        the permissions are set to 0755 for the deploy user. When starting the application directly on the server (I am the admin there) in the directory /var/www/preview.my_app/current, the following output is received:

        $/var/www/preview.my_app/current: puma start
        * => Running the jungle… * –> Woke up puma /var/www/preview.my_app/current * user aw * log to /var/www/preview.my_app/current/log/puma.log Starting /usr/local/bin/run-puma…
        Detaching to start /usr/local/bin/run-puma…done.

        But there is no puma process and nginx is still responding with a 502

        Is it maybe a problem, that nginx is still running with activated phusion_passenger? Do I have to reinstall nginx?

        Thanks a lot for your help!

        Andy

      2. tommychheng Post author

        Did the puma log file /var/www/preview.my_app/current/log/puma.log say anything?

        I don’t think it’s a problem with nginx if the puma process didn’t even start.

        Maybe there’s a syntax error in your puma config?

      3. andywenk

        the puma.log is empty (!?). What I have for the puma config in /etc/puma.conf is

        /var/www/preview.my_app/current,aw

        Then I run

        puma start

        as user aw and I receive:

        * => Running the jungle… * –> Woke up puma /var/www/preview.my_app/current * user aw * log to /var/www/preview.my_app/current/log/puma.log Starting /usr/local/bin/run-puma…
        Detaching to start /usr/local/bin/run-puma…done.

        But the log is still 0 byte size and requesting the URL is resulting in a 502. The enginx error log says:

        2013/11/26 10:42:09 [crit] 15199#0: *33107 connect() to unix:///var/www/preview.my_app/shared/tmp/puma/preview.my_app.sock failed (2: No such file or directory) while connecting to upstream, client: 213.203.242.220, server: preview.my_app, request: “GET / HTTP/1.1″, upstream: “http://unix:///var/www/preview.my_app/shared/tmp/puma/qraex_preview.sock:/”, host: “preview.my_app”

        and there is no puma process running ….

        Hm – I am really stuck here … your help is highly appreciated (if you don’t want to flood the comments here, I can move this to stackoverflow if you like)

      4. tommychheng Post author

        Try to determine if the problem lies at the puma part which i think does.

        Can you STOP nginx/passenger and just run puma on port 80
        bundle exec puma -p 80
        in your rails directory?

        Then, check the log files.

        Also check the config/puma.rb for correct syntax.

        It looks like puma never started, thats why there’s no entries in the log files.

      5. Andreas Wenk

        Tommy,

        sry for comming back that late. Thanks for all your help. puma did never start. And as i wantetd support for SPDY, I updated the passenger / nginx installation. This is cool for now but I will definitely give puma a try when I set up a fresh machine.

        Cheers

        Andy

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s