Category Archives: Ruby

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.

Debugging ActionView::MissingTemplate exception in Rails 3.1

We got an ActionView::MissingTemplate exception from a remote site using our embed code.

The exception was:

ActionView::MissingTemplate: Missing template /embed, application/embed with {:handlers=>[:erb, :builder, :haml], :formats=>["*/*;q=0.01"], :locale=>[:en, :en]}.

with these http headers:

HTTP_ACCEPT "*/*;q=0.01"
HTTP_ACCEPT_LANGUAGE "en"
HTTP_USER_AGENT "Mozilla/4.0 (PSP (PlayStation Portable); 2.00)"

The strange thing is that PSP is sending us this accept header:
HTTP_ACCEPT "*/*;q=0.01";

HTTP_ACCEPT is a http request header used by the client asking for the types of formats it can support. Typically, browsers send an list of acceptable formats. Google Chrome sends Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 which means that the server should try to send back an html or xml format with a preference value of q=0.9 and if not available, send anything else(*/*) with a preference value of q=0.8.

Unfortunately, since our Rails controller code explicitly only accepted html or json with a respond_to block, Rails didn’t interpret “*/*” as html.

respond_to :html, :json
render :layout => false

We can make the fix by explicitly render the default format as html:

render "embed.html", :layout => false

Solrsan: Lightweight Solr Gem for Ruby on Rails 3 Applications

I decided to create Solrsan to use the Apache Solr search server in my various Rails 3 applications. Currently, there are two main ruby gems for using Apache Solr in a ruby project:

  • rsolr: RSolr is a low level layer to Apache Solr. Because it’s meant to be just an access layer, rsolr is missing the configuration setup such as the schema.xml, solrconfig.xml, etc which is custom per each Ruby/Rails app.
  • sunspot: Sunspot is an all in one solution for using solr with a ruby project. It even uses rsolr under the hood.

Generally, I like API access layers to be as similar to the raw api as possible. Sunspot’s api works using a search block:

Post.search do
  fulltext 'best pizza'
  with :blog_id, 1  
  with(:published_at).less_than Time.now
  order_by :published_at, :desc
  paginate :page => 2, :per_page => 15
  facet :category_ids, :author_id
end

The actual query becomes an http get request. Solr itself is just a Java servlet which just reads http requests and responses with json/xml/other formats. I prefer using rsolr’s style of access because it’s most similar to http requests:

  response = solr.get 'select', :params => {
    :q=>'washington',
    :start=>0,
    :rows=>10
  }

Solrsan also uses rsolr under the hood and adds a few extra functionality. For example, when you want to add solr functionality to your ruby/rails app, you need its own set of config files, a way to start/stop the solr server, a way to deploy using capistrano. Solrsan comes with these basic setup files to help you get started.

Indexing

To index objects, edit config/solr/conf/schema.xml to state the types of fields you want to index. Or you can use dynamic fields to avoid specifying new fields each time.

Then, include Solrsan::Search into your model(Activerecord, mongoid, etc) and define a method called as_solr_document which returns a hash of the key-value pair entries to index. See the README for more examples.

You can add an after_save method to call the index method as well. I did not automatically add the index method on every object save since some systems may need index via a different method such as via a queuing system.

Searching

Search is as easy as:
response = Document.search(:q => "hello world")
This will return a hashmap response composed of docs and metadata. response[:docs] will be a will_paginated object collection and response[:metadata] contains various supporting items such as error messages, facets, etc.

Summary

So I decided not to use sunspot because I want a transparent API access over a DSL implementation and I needed something more than the basic rsolr gem.

If you are interesting in using solrsan, the important links are the readme and unit tests. I’m already using solrsan on a few projects but it is still relatively new. Feel free to email/pull request any problems/bugs!