Setup Development Environment for Rack-based applications
Order of Installation
- Nginx
- RVM
- Ruby
- Unicorn
plus their dependencies.
Install Nginx
Mac OS X
Just use Homebrew… seriously.
$ brew install nginx
Ubuntu Linux
$ sudo apt-get install nginx
Install RVM
$ curl -L get.rvm.io | bash -s stable
$ source ~/.rvm/scripts/rvm
If cURL complains about SSL certification error, download the following certificate:
$ wget http://curl.haxx.se/ca/cacert.pem
$ export CURL_CA_BUNDLE=~/cacert.pem
To install a version of Ruby, say 1.9.3:
$ rvm install 1.9.3
NOTE: There may be some issues with installing version 1.9.3 due to the change in the default YAML parser. I encountered this in my CentOS 5.7 setup and couldn’t make it work despite trying all the different solutions provided by other users. Ended up using 1.8.7 instead.
UPDATE (22 Oct 2012): Found the fix!
$ rvm pkg install libyaml
$ rvm pkg install readline
$ rvm install 1.9.3 --with-readline-dir=~/.rvm/usr --with-libyaml-dir=~/.rvm/usr
To set a version as default:
$ rvm use <version> --default
To load RVM into shell session permanently, put the following at the end of the bash profile (/.bashrc):
$ vim ~/.bashrc
...
[[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm"
(Optional) Create Project-Specific Gemsets
RVM allows for installations of multiple Ruby versions on the same machine, with a set of gems installed in default locations for each version.
RVM also allows for each individual version to have multiple gemsets applied to it, as required by different projects, for example.
To create and switch to a newly created gemset, e.g. a gemset called rails3 running on Ruby 1.8.7:
$ rvm use 1.8.7@rails3 --create
Now we can proceed to install gems in this new gemset. For example, if we want to test Rails 3.1.3 on Ruby 1.8.7:
$ gem install rails --version 3.1.3
To see the gems installed in this gemset:
$ gem list --local
It is also possible to copy gemsets:
$ rvm gemset copy <version1>@<gemset1> <version2>@<gemset2>
Read RVM Best Practices to manage each project better.
Initial Configuration
For simplicity, we will create a “Hello World” application on Sinatra, but this should work on any Rack-based setup.
$ gem install sinatra
$ sudo mkdir -p /var/www/helloworld
$ sudo vim /var/www/helloworld/app.rb
require 'sinatra'
get '/' do
"Hello! The time now is #{Time.now}."
end
Might be good to create a new group called “web” for handling all these web-based files:
$ sudo groupadd web
$ sudo usermod -a -G web nginx
$ sudo chown nginx:web -R web /var/www
$ sudo chmod -R 775 /var/www
Add yourself to group “web”:
$ sudo usermod -a -G web <username>
Ensure that port 80 (or whichever port you want to use in Nginx) is open:
$ sudo cat /etc/sysconfig/iptables
...
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT
Setup config.ru
$ sudo vim /var/www/helloworld/config.ru
require 'sinatra'
set :env, :production
disable :run
require './app.rb'
run Sinatra::Application
Setup Unicorn configuration file
Please refer to links in References for better understanding on what each line is doing.
$ sudo vim unicorn.config
worker_processes 4
working_directory "/var/www/helloworld"
listen 'unix:/tmp/unicorn.sock', :backlog => 512
timeout 120
pid "/tmp/basic_unicorn.pid"
# This loads the application in the master process before forking worker procs
preload_app true
if GC.respond_to?(:copy_on_write_friendly=)
GC.copy_on_write_friendly = true
end
##
# 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.
before_fork do |server, worker|
old_pid = "#{server.config[:pid]}.oldbin"
if File.exists?(old_pid) && server.pid != old_pid
begin
Process.kill("QUIT", File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
# someone else did our job for us
end
end
end
# Set the path of the log files inside the log folder of the testapp
stderr_path "/path/to/unicorn.stderr.log"
stdout_path "/path/to/unicorn.stdout.log"
Deploy Rack application
Now we can deploy our “Hello World” Sinatra application on Unicorn:
$ unicorn -c unicorn.config &
In this case, the “Hello World” application is listening to the socket /tmp/unicorn.sock as defined in unicorn.config above. The next step is to “link” it in our Nginx configuration.
Setup Nginx configuration file
$ sudo vim /etc/nginx/nginx.conf
events {
worker_connections 1024;
accept_mutex off;
use epoll;
}
...
upstream app_server {
server unix:/tmp/unicorn.sock fail_timeout=0;
}
server {
listen 80 default deferred;
client_max_body_size 4G;
server_name _;
keepalive_timeout 5;
root /var/www/helloworld;
try_files $uri/index.html $uri.html $uri @app;
location @app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app_server;
}
}
The “link” to the “Hello World” application deployed on Unicorn is defined in the upstream part. The socket file must match the one defined in unicorn.config.
Restart Nginx:
$ sudo service nginx restart
Ensure that the socket is created:
$ sudo netstat -nalx | grep unicorn.sock
Ensure that port 80 (or whichever port you want to use in Nginx) is open and receiving requests:
$ sudo netstat - nalt | grep :80
If everything is setup correctly, any HTTP requests to http://<domain_name> will be redirected to our “Hello World” application.
References
- Unicorn! - The GitHub Blog
- Nginx + Unicorn sample configuration
- Setting up Unicorn with Nginx
- Running Sinatra (and other Rack apps) on Nginx + Unicorn