mystix
3/8/2012 - 12:28 PM

Ruby on Rails server setup on Ubuntu 11.04 with Nginx, Unicorn, Rbenv

Ruby on Rails server setup on Ubuntu 11.04 with Nginx, Unicorn, Rbenv

# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete documentation
#
# This file should go in the config directory of your Rails app e.g. config/unicorn.rb

app_dir = "/home/example.co.uk/website/"
worker_processes 5 
working_directory app_dir

# Load app into the master before forking workers for super-fast
# worker spawn times
preload_app true

# nuke workers after 60 seconds (the default)
timeout 60

# listen on a Unix domain socket and/or a TCP port,

#listen 8080 # listen to port 8080 on all TCP interfaces
#listen "127.0.0.1:8080"  # listen to port 8080 on the loopback interface
listen "/tmp/example.co.uk.socket"

# feel free to point this anywhere accessible on the filesystem
user 'example.co.uk', 'example.co.uk'

pid "#{app_dir}/pids/unicorn.pid"
stderr_path "#{app_dir}/log/unicorn.stderr.log"
stdout_path "#{app_dir}/log/unicorn.stdout.log"

# http://www.rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
if GC.respond_to?(:copy_on_write_friendly=)
  GC.copy_on_write_friendly = true
end


before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!

  ##
  # When sent a USR2, Unicorn will suffix its pidfile with .oldbin and
  # immediately start loading up a new version of itself (loaded with a new
  # version of our app). When this new Unicorn is completely loaded
  # it will begin spawning workers. The first worker spawned will check to
  # see if an .oldbin pidfile exists. If so, this means we've just booted up
  # a new Unicorn and need to tell the old one that it can now die. To do so
  # we send it a QUIT.
  #
  # Using this method we get 0 downtime deploys.

  old_pid = "#{server.config[:pid]}.oldbin"

  if File.exists?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  # Unicorn master loads the app then forks off workers - because of the way
  # Unix forking works, we need to make sure we aren't using any of the parent's
  # sockets, e.g. db connection

  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
  # Redis and Memcached would go here but their connections are established
  # on demand, so the master never opens a socket
end

Server Commissioning

Ubuntu mainstream packages are pretty out of date for nginx; we want version > 1.0, so we need to reference repository that has more recent versions before we install:

$ add-apt-repository ppa:nginx/stable && apt-get update

Update, upgrade and install nginx and development tools:

$ apt-get -y install nginx git-core build-essential

Extras for RubyGems and Rails:

$ apt-get -y install zlib1g-dev
$ apt-get -y install libssl-dev libsqlite3-dev
$ apt-get -y install libreadline5-dev
$ apt-get -y install curl

Add a deployment user:

$ useradd -m -g staff -s /bin/bash deployer
$ passwd deployer

Create a custom shudders file, and add the following line (sudo vi /etc/sudoers.d/our-company):

%staff ALL=(ALL) ALL

Nginx

Edit /etc/nginx/proxy_params and add shared proxy config settings (optional)

proxy_set_header Host $host;

# needed to forward user's IP address to application server
proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header   X-Forwarded-Proto $scheme;

proxy_send_timeout         90;
proxy_read_timeout         90;

proxy_buffer_size          4k;
proxy_buffers              4 32k;
proxy_busy_buffers_size    64k;
proxy_temp_file_write_size 64k;

Ruby & Rails Setup ror each User / App

$ sudo adduser --shell /bin/bash example-user
$ su - example-user
$ cd ~example-user

Install rbenv:

Check out rbenv into ~/.rbenv :

$ git clone git://github.com/sstephenson/rbenv.git .rbenv

Add ~/.rbenv/bin to your $PATH for access to the rbenv command-line :

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>   

$ ~/.bash_profile

Add rbenv init to your shell to enable shims and autocompletion:

$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

Restart your shell so the path changes take effect in order to use rbenv:

$ exec $SHELL

If the above shell reload doesn't give you the rbenv command, then you will have to exit and re-enter the shell

Use rbenv to install a specific Ruby version :

$ rbenv install 1.9.2-p290

Rebuild the shim binaries. You should do this any time you install a new Ruby binary e.g. when installing a new Ruby version, or when installing a gem that provides a binary:

$ rbenv rehash

Set a global Ruby version for all shells:

$ rbenv global 1.9.2-p290

Unicorn

$ gem install bundler unicorn --no-rdoc --no-ri; rbenv rehash

Add the following environment config variables to a file at /etc/unicorn/example.co.uk.conf (The Unicorn process will look here):

RAILS_ROOT=/home/example.co.uk/website
RAILS_ENV=production

Clone your app into ~/website (depends upon if you are using git, or some other source), and then install your bundle:

$ cd ~/website
$ bundle install

create app-specific unicorn init file here and make it executable (See init file in this gist):

$ sudo chmod +x /etc/init.d/unicorn_example.co.uk  

Add a unicorn.rb app config file to your Rails app at config/unicorn.rb (Example attached). Edit the file to match the directory path to your app, and user names and groups. Also uncomment the 'listen' directive in the file for listening on a TCP port.

Check that unicorn can be started:

$ /etc/init.d/unicorn_example.co.uk start

Check that unicorn is listening on the configured port (8080 in this example):

$ netstat -natp | grep unicorn

You should see something like:

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      5272/unicorn.rb -E 

Okay, if that's all good, then you can comment out the TCP port listening directive in config/unicorn.rb, so that unicorn worker processes are only accessible to Nginx via Unix socket.

Nginx virtual hosts

Create an Nginx virtual hosts configuration file in 'sites-available' and enter contents of nginx_virtual_host file:

$ sudo vi /etc/nginx/sites-available/example.co.uk

Create a symlink from sites-available to site-enabled:

$ sudo ln -s /etc/nginx/sites-available/example.co.uk /etc/nginx/sites-enabled/example.co.uk
upstream example-workers {
    # fail_timeout=0 means we always retry an upstream even if it failed
    # to return a good HTTP response (in case the Unicorn master nukes a single worker for timing out).
    server unix:/tmp/example.co.uk.socket fail_timeout=0;
}

server {
  listen                80; # default;
  server_name           example.co.uk;
  root                  /home/example.co.uk/website/public;

  location / {
    access_log          off;

    include proxy_params;
    proxy_redirect off;

    if (-f $request_filename) {
      access_log          off;
      expires             max;
      break;
    }

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

    if (!-f $request_filename) {
      proxy_pass          http://example-workers;
      break;
    }
  }
}
#! /bin/bash

### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the unicorn web server
# Description:       starts unicorn
### END INIT INFO

USER=example.co.uk
PATH=/home/$USER/.rbenv/bin:/home/$USER/.rbenv/shims:$PATH
DAEMON=unicorn
DAEMON_OPTS="-c /home/$USER/website/config/unicorn.rb -E production -D"
NAME=unicorn
DESC="Unicorn app for $USER"
PID=/home/$USER/website/tmp/pids/unicorn.pid

case "$1" in
  start)
        CD_TO_APP_DIR="cd /home/$USER/website"
        START_DAEMON_PROCESS="bundle exec $DAEMON $DAEMON_OPTS"

        echo -n "Starting $DESC: "
        if [ `whoami` = root ]; then
          su - $USER -c "$CD_TO_APP_DIR > /dev/null 2>&1 && $START_DAEMON_PROCESS"
        else
          $CD_TO_APP_DIR > /dev/null 2>&1 && $START_DAEMON_PROCESS
        fi
        echo "$NAME."
        ;;
  stop)
        echo -n "Stopping $DESC: "
        kill -QUIT `cat $PID`
        echo "$NAME."
        ;;
  restart)
        echo -n "Restarting $DESC: "
        kill -USR2 `cat $PID`
        echo "$NAME."
        ;;
  reload)
        echo -n "Reloading $DESC configuration: "
        kill -HUP `cat $PID`
        echo "$NAME."
        ;;
  *)
        echo "Usage: $NAME {start|stop|restart|reload}" >&2
        exit 1
        ;;
esac

exit 0