Skip to content

Instantly share code, notes, and snippets.

@scottlowe
Created October 21, 2011 10:48
Show Gist options
  • Save scottlowe/1303554 to your computer and use it in GitHub Desktop.
Save scottlowe/1303554 to your computer and use it in GitHub Desktop.
Ruby on Rails server setup on Ubuntu 11.04 with Nginx, Unicorn, Rbenv
#! /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/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
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;
}
}
}

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:

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

Update, upgrade and install nginx and development tools:

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

Extras for RubyGems and Rails:

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

# Note: For Ubuntu 11.10, you will probably require 'libreadline-gplv2-dev' instead

Add a deployment user:

$ sudo useradd -m -g staff -s /bin/bash deployer
$ sudo 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 or create /etc/nginx/proxy_params and add shared proxy config settings. You don't have to have these in a separate file, but it will mean you don't have to repeat yourself if you have other apps on the same server. If you don't want the extra file, then put these proxy settings into your apps nginx conf file instead of include proxy_params;.

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 Ruby via 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

Official Rbenv docs say "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 (it never works for me), then you will have to exit and re-enter the shell.

A minor detour: Installing ruby-build to support Rbenv...

$ git clone git://github.com/sstephenson/ruby-build.git
$ cd ruby-build
$ ./install.sh

This will install ruby-build into /usr/local. If you do not have write permission to /usr/local, you will need to run sudo ./install.sh instead. You can install to a different prefix by setting the PREFIX environment variable.

... and back to Rbenv

Use rbenv to install a specific Ruby version
:

$ rbenv install 1.9.3-p0

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.3-p0

Install Rails, Bundler and additionally a JavaScript runtime if you are using >= Rails 3.1 and are using the asset pipeline

$ gem install rails bundler execjs --no-rdoc --no-ri; rbenv rehash

Unicorn

$ gem install 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

Automatic startup of unicorn workers

In production, you probably want your unicorn workers to start up automatically during the server boot process.

$ sudo update-rc.d unicorn_example.co.uk defaults
# 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 10
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"
# Don't set user if you are already running as the user (will cause a massive chown loop of death)
# This is for if you execute as root and become user.
#user 'example.co.uk', 'example.co.uk'
pid "/home/example.co.uk/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment