15.4. Web Server with Capistrano Deployment

In Section 15.3, “Webserver Without Deployment”, we have created a new Rails project on the production system. Logically, this is not the normal approach. Normally, one or several developers would develop the Rails project on their own computers and then upload it to the production server later.
Especially if you are working with several developers, it makes sense to use a version management system. With Capistrano, you can then use this version management to distribute updates of the Rails project to one or several web servers. In this chapter, we are using Git as distributed version management and Github (http://github.com) as server for hosting Git.
For this tutorial, you will need a ready installed production server with nginx and a Ruby 1.9.3 and Rails 3.2 installed with RVM with the user deployer (for a detailled guide, please read Section 15.2, “Basic Installation Web Server”).

Development System

We start again with a new Rails application. Please create this application on your development computer.

rails new blog

We create the mini blog Rails application:
Stefan-Wintermeyers-MacBook-Air:~ xyz$ rails new blog
[...]
Stefan-Wintermeyers-MacBook-Air:~ xyz$ cd blog
Stefan-Wintermeyers-MacBook-Air:blog xyz$ rails generate scaffold post subject content:text
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$

Note

If you want to work with this system in the development environment, you still need a
Stefan-Wintermeyers-MacBook-Air:blog xyz$ rake db:migrate
==  CreatePosts: migrating ====================================================
-- create_table(:posts)
   -> 0.0015s
==  CreatePosts: migrated (0.0016s) ===========================================

Stefan-Wintermeyers-MacBook-Air:blog xyz$
Required Gems for Deployment
For the deployment and the web server, we need some gems. Please insert this configuration into the file Gemfile:
source 'https://rubygems.org'

gem 'rails', '3.2.6'

gem 'sqlite3'

# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'sass-rails',   '~> 3.2.3'
  gem 'coffee-rails', '~> 3.2.1'

  # See https://github.com/sstephenson/execjs#readme for more supported runtimes
  # gem 'therubyracer', :platforms => :ruby

  gem 'uglifier', '>= 1.0.3'
end

gem 'jquery-rails'

# To use ActiveModel has_secure_password
# gem 'bcrypt-ruby', '~> 3.0.0'

group :production do
  # Use MySQL as the production database
  gem 'mysql'

  # Use unicorn as the app server
  gem 'unicorn'
end


group :development do
  # Use Capistrano for the deployment
  gem 'capistrano'
  gem 'rvm-capistrano'
end
Then execute a bundle install:
Stefan-Wintermeyers-MacBook-Air:blog xyz$ bundle install
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$

Unicorn Configuration

For the Unicorn configuration, we use the file https://raw.github.com/defunkt/unicorn/master/examples/unicorn.conf.rb as basis, adapt it to our server as follows and save it in the file config/unicorn.rb:
# Use at least one worker per core if you're on a dedicated server,
# more will usually help for _short_ waits on databases/caches.
worker_processes 4

# Since Unicorn is never exposed to outside clients, it does not need to
# run on the standard HTTP port (80), there is no reason to start Unicorn
# as root unless it's from system init scripts.
# If running the master process as root and the workers as an unprivileged
# user, do this to switch euid/egid in the workers (also chowns logs):
user "deployer", "www-data"

# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
APP_PATH = "/var/www/blog/current"
working_directory APP_PATH

# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
listen "/tmp/unicorn.blog.sock", :backlog => 64
listen 8080, :tcp_nopush => true

# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 30

# feel free to point this anywhere accessible on the filesystem
pid APP_PATH + "/tmp/pids/unicorn.pid"

# By default, the Unicorn logger will write to stderr.
# Additionally, ome applications/frameworks log to stderr or stdout,
# so prevent them from going to /dev/null when daemonized here:
stderr_path APP_PATH + "/log/unicorn.blog.stderr.log"
stdout_path APP_PATH + "/log/unicorn.blog.stdout.log"

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
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.connection.disconnect!
  end

  # Before forking, kill the master process that belongs to the .oldbin PID.
  # This enables 0 downtime deploys.
  old_pid = "/tmp/unicorn.my_site.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

after_fork do |server, worker|
  # the following is *required* for Rails + "preload_app true",
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end

  # if preload_app is true, then you may also want to check and
  # restart any other shared sockets/descriptors such as Memcached,
  # and Redis.  TokyoCabinet file handles are safe to reuse
  # between any number of forked children (assuming your kernel
  # correctly implements pread()/pwrite() system calls)
end

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = "/var/www/blog/current/Gemfile"
end

Capistrano Configuration

We set up a Capistrano standard configuration:
Stefan-Wintermeyers-MacBook-Air:blog xyz$ capify .  
[add] writing './Capfile'
[add] writing './config/deploy.rb'
[done] capified!
Stefan-Wintermeyers-MacBook-Air:blog xyz$
Then we set up the config/deploy.rb with the following content. Please remember to replace the text ip.address.of.server with the IP address of your web server!
require "bundler/capistrano"
require "rvm/capistrano"
set :rvm_ruby_string, '1.9.3'

server "ip.address.of.server", :web, :app, :db, primary: true

set :application, "blog"
set :user, "deployer"
set :deploy_to, "/var/www/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false

set :scm, "git"
set :repository, "git@github.com:your_github_account/#{application}.git"
set :branch, "master"

default_run_options[:pty] = true
ssh_options[:forward_agent] = true

after 'deploy', 'deploy:cleanup'
after 'deploy', 'deploy:migrate'

namespace :deploy do
  %w[start stop restart reload].each do |command|
    desc "#{command} unicorn server"
    task command, roles: :app, except: {no_release: true} do
      run "sudo /etc/init.d/unicorn_#{application} #{command}"
    end
  end

  # Use this if you know what you are doing.
  #
  # desc "Zero-Downtime restart of Unicorn"
  # task :restart, :except => { :no_release => true } do
  #   run "sudo /etc/init.d/unicorn_#{application} reload"
  # end
end
Now change the file Capfile as follows:
load 'deploy'
# Uncomment if you are using Rails' asset pipeline
load 'deploy/assets'
Dir['vendor/gems/*/recipes/*.rb','vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) }
load 'config/deploy' # remove this line to skip loading any of the default tasks

Setting Up Github as Repository

Please create a new account at https://github.com or use an existing Github account. Use this account to create a new repository with the name "blog".

Tip

To simplify your work, I recommend that you enter your SSH key in your Github account at https://github.com/settings/ssh.
Now you can commit and push your project. Of course, you need to replace your_github_account with your own Github account:
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git init
Initialized empty Git repository in /Users/xyz/blog/.git/
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git add .
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git commit -m 'first commit'
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git remote add origin git@github.com:your_github_account/blog.git
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git push -u origin master
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$
Your Rails project is now hosted in a Github repository and you can view it at https://github.com/your_github_account/blog.

Web Server

You need to carry out the following steps on the web server system.

Generate SSH Key

We create a public SSH key for the user deployer. Please log in to the web server as user deployer. Deployment is easier later on if you use an empty pass phrase.
deployer@debian:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/deployer/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/deployer/.ssh/id_rsa.
Your public key has been saved in /home/deployer/.ssh/id_rsa.pub.
The key fingerprint is:
ba:11:90:2a:e3:8f:5b:2e:70:99:50:86:a1:9a:2c:b7 deployer@debian
The key's randomart image is:
+--[ RSA 2048]----+
|.o               |
|o o  .           |
|.o  o            |
|+. . .           |
|*ooo  . S        |
|+++.   o         |
|.oE.  o          |
| .=    o         |
| ooo  .          |
+-----------------+
deployer@debian:~$ cat .ssh/id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJEGixOcPRdBMry7PPG/Rgla50EM+JPKGYGD/yJ8v7bdrfT68t2/eVbj6+YebWh1tRebE3qqouqmZjIlocr1j67SmfXZ/sswBT/pXOhP89JtHPMolx7rUQ8wQF3aDrnVDJG0gdvRm212vN2bou3N5dzhekmWmbS3R0ZGNM9ZgTw8rhTOd1M2QVTzyV1i1PehoFxOu1WIc1gN5C42zihbJ6fGgVb45WeKzXSi6bQ6PMKD1gAMJpXHPvKLhi0wLN0wNOJwa6BKR3pmgICSBuoziAhhCS/7gBDJnqRmx1zax/1CShJD3QEGHvofA9okYuYVqyrJi1hdF8ZgMnQCb31I21 deployer@debian
deployer@debian:~$
The generated key is located in the file /home/deployer/.ssh/id_rsa.pub.

Important

Please now log in to your Github account and add this key under Deploy Keys in the admin area of your Github project. See https://github.com/your_github_account/blog/admin/keys
Then use ssh to connect on the console with the Github SSH server and confirm the question "Are you sure you want to continue connection (yes/no)?" with yes.
deployer@debian:~$ ssh git@github.com
The authenticity of host 'github.com (207.97.227.239)' can't be established.
RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'github.com,207.97.227.239' (RSA) to the list of known hosts.
PTY allocation request failed on channel 0
Hi your_github_account/blog! You've successfully authenticated, but GitHub does not provide shell access.
                 Connection to github.com closed.
deployer@debian:~$
Now you can start deploying on your development system.

cap deploy:setup

With cap deploy:setup we can set up the necessary directory structure on the target system. If you have not stored your SSH key on the target system, the script will ask you for the password for the user deployer. Depending on your connection and CPU power, this initial setup step may take longer.
Stefan-Wintermeyers-MacBook-Air:blog xyz$ cap deploy:setup
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$ 
After the cap deploy:setup, you will find the following directory structure on your web server:
/var/www/blog
├── releases
└── shared
    ├── log
    ├── pids
    └── system
We can start the first deploy with cap deploy:
Stefan-Wintermeyers-MacBook-Air:blog xyz$ cap deploy
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$ 
We then see the result of the deploy in the web server's directory structure:
/var/www/blog
├── current -> /var/www/blog/releases/20120711131031
├── releases
│   └── 20120711131031
└── shared
    ├── assets
    ├── bundle
    ├── cached-copy
    ├── log
    ├── pids
    └── system
Capistrano has created a new directory for the new application version in the subdirectory /var/www/blog/releases and linked this new directory to the directory /var/www/blog/current.

Web Server Configuration

Now we still need to write and activate an init script and configuration file on the web server.

Unicorn Init Script

Please log in to the web server as user root and create the init script /etc/init.d/unicorn_blog with the following content:
#!/bin/bash

### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Unicorn webserver
# Description:       Unicorn webserver for the blog
### END INIT INFO

UNICORN=/home/deployer/.rvm/bin/bootup_unicorn
UNICORN_ARGS="-D -c /var/www/blog/current/config/unicorn.rb -E production"
KILL=/bin/kill
PID=/var/www/blog/shared/pids/unicorn.pid

sig () {
  test -s "$PID" && kill -$1 `cat $PID`
}

case "$1" in
        start)
                echo "Starting unicorn..."
                $UNICORN $UNICORN_ARGS
                ;;
        stop)
                sig QUIT && exit 0
                echo >&2 "Not running"
                ;;
        reload)
                sig USR2 && exit 0
                ;;
        restart)
                $0 stop
                $0 start
                ;;
        status)
                ;;
        *)
                echo "Usage: $0 {start|stop|reload|restart|status}"
                ;;
esac
Now we need to activate the init script and start Unicorn:
root@debian:~# chmod +x /etc/init.d/unicorn_blog 
root@debian:~# update-rc.d -f unicorn_blog defaults
update-rc.d: using dependency based boot sequencing
root@debian:~# /etc/init.d/unicorn_blog start
root@debian:~# 
Your Rails project is now accessible via the web server's IP address.

nginx Configuration

For the Rails project, we add a new configuration file /etc/nginx/conf.d/blog.conf with the following content:
upstream unicorn {
  server unix:/tmp/unicorn.blog.sock fail_timeout=0;
}

server {
  listen 80 default deferred;
  # server_name example.com;
  root /var/www/blog/current/public;

  location / {
    gzip_static on;
  }

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @unicorn;
  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://unicorn;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 4G;
  keepalive_timeout 10;
}
We rename the default configuration file to make sure it is no longer executed. Then we restart nginx.
root@debian:~# mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.backup
root@debian:~# /etc/init.d/nginx restart
Restarting nginx: nginx.
root@debian:~#

sudo

In the Capistrano deploy script, we need a way of stopping and starting Unicorn via init script. So we need to install sudo on the web server:
root@debian:~# apt-get install sudo
[...]
root@debian:~# 
And in the file /etc/sudoers we need to add the following line:
deployer ALL= NOPASSWD: /etc/init.d/unicorn_blog

Deployment

The big advantage of working with Capistrano is that it makes it so easy to install new versions (deployment). You can simply do it from the development system via the command cap deploy. After a second cap deploy, the directory structure looks like this:
/var/www/blog
├── current -> /var/www/blog/releases/20120711132357
├── releases
│   ├── 20120711131031
│   └── 20120711132357
└── shared
    ├── assets
    ├── bundle
    ├── cached-copy
    ├── log
    ├── pids
    └── system
So the relevant releases are always saved in the directory /var/www/blog/releases.
In the directory /var/www/blog/shared you will find directories that are shared by the relevant release. These are always linked automatically within /var/www/blog/current.
In the configuration we are using, the last 5 releases are automatically saved, any older releases are deleted.

Tip

Capistrano is a very powerful tool. It is well worth it for any admin to have a look at the Capistrano Wiki (https://github.com/capistrano/capistrano/wiki) zu werfen.

Updates about this book will be published on my Twitter feed.