@ryanbigg asked me how I set up deployment, figured I'd post this internal guide we've been using.

Derived from this link

The main difference is that this configuration supports rails 3.2.

To set up a server, do the following as root:

echo '--- Install system packages ---'
apt-get update

Let that finish. Then:

apt-get upgrade -y

Let that finish. Then:

apt-get install build-essential ruby-full libmagickcore-dev imagemagick libxml2-dev libxslt1-dev git-core postgresql postgresql-client postgresql-server-dev-8.4 nginx curl node

Let that finish. Then:

apt-get build-dep ruby1.9.1

Install node so you can do asset precompilation later:

sudo apt-get install python-software-properties && sudo add-apt-repository ppa:chris-lea/node.js && sudo apt-get update && sudo apt-get install nodejs nodejs-dev

Then make a deployer user:

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

Then fill in the deployer password.

Add this to /etc/sudoers:

%staff ALL=(ALL) ALL

Install system-wide rvm (as root)

bash -s stable < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer)

Install ruby

source /etc/profile.d/rvm.sh
rvm install 1.9.2
rvm use 1.9.2@projectname --create
gem install bundler

As your deployer user, add this to your ~/.rvmrc

rvm_trust_rvmrcs_flag=1

Add the following to /etc/environment to always run commands in production on this server.

RAILS_ENV=production

Setup postgres database:

sudo -u postgres createdb projectname_production
sudo -u postgres psql

Then execute the following SQL (use your own password):

CREATE USER projectname_production WITH PASSWORD 'isotope_bang';
GRANT ALL PRIVILEGES ON DATABASE projectname_production TO projectname_production;

Make sure this user can actually log in (by default, only local-system users can). Modify /etc/postgresql/9.1/main/pg_hba.conf and change out the line requiring local users to use ident to md5.

local   all         all                               md5

Restart postgres

/etc/init.d/postgresql restart

Then set up nginx:

# /etc/nginx/sites-available/default
upstream projectname.com {
  # 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).

  # for UNIX domain socket setups:
  server unix:/tmp/projectname.com.socket fail_timeout=0;
}

server {
    # if you're running multiple servers, instead of "default" you should
    # put your main domain name here
    listen 80 default;

    # you could put a list of other domain names this application answers
    server_name projectname.com;

    root /home/deployer/apps/projectname.com/current/public;
    access_log /var/log/nginx/projectname.com_access.log;
    rewrite_log on;

    location / {
        #all requests are sent to the UNIX socket
        proxy_pass  http://projectname.com;
        proxy_redirect     off;

        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

        client_max_body_size       10m;
        client_body_buffer_size    128k;

        proxy_connect_timeout      90;
        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;
    }

    # if the request is for a static resource, nginx should serve it directly
    # and add a far future expires header to it, making the browser
    # cache the resource and navigate faster over the website
    # this probably needs some work with Rails 3.1's asset pipe_line
    location ~ ^/(images|javascripts|stylesheets|system|assets)/  {
      root /home/deployer/apps/projectname.com/current/public;
      expires max;
      break;
    }
}

And then:

# /etc/nginx/nginx.conf 
user deployer staff;

# Change this depending on your hardware
worker_processes 4;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
}

http {
    types_hash_bucket_size 512;
    types_hash_max_size 2048;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay off;
    # server_tokens off;

    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    gzip on;
    gzip_disable "msie6";

    # gzip_vary on;
    gzip_proxied any;
    gzip_min_length 500;
    # gzip_comp_level 6;
    # gzip_buffers 16 8k;
    # gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    ##
    # Virtual Host Configs
    ##

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Now start nginx (won't work till unicorn's up but go ahead)

/etc/init.d/nginx start

Add unicorn to your project's Gemfile:

# Gemfile
gem "unicorn"

group :development do
  gem "capistrano"
end

Add unicorn config to your project:

# config/unicorn.rb
# Set environment to development unless something else is specified
env = ENV["RAILS_ENV"] || "development"

# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.
worker_processes 4

# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
listen "/tmp/my_site.socket", backlog: 64

# Preload our app for more speed
preload_app true

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

pid "/tmp/unicorn.my_site.pid"

# Production specific settings
if env == "production"
  # Help ensure your application will always spawn in the symlinked
  # "current" directory that Capistrano sets up.
  working_directory "/home/deployer/apps/my_site/current"

  # feel free to point this anywhere accessible on the filesystem
  user 'deployer', 'staff'
  shared_path = "/home/deployer/apps/my_site/shared"

  stderr_path "#{shared_path}/log/unicorn.stderr.log"
  stdout_path "#{shared_path}/log/unicorn.stdout.log"
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
  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

Capify the project:

capify .

Replace config/deploy.rb with the following:

# config/deploy.rb 
require "bundler/capistrano"

set :application,     "projectname.com"
set :scm,             :git
set :repository,      "git@github.com:isotope11/projectname.com.git"
set :branch,          "origin/master"
set :migrate_target,  :current
set :ssh_options,     { forward_agent: true }
set :rails_env,       "production"
set :deploy_to,       "/home/deployer/apps/projectname.com"
set :normalize_asset_timestamps, false

set :user,            "deployer"
set :group,           "staff"
set :use_sudo,        false

role :web,    "192.168.1.154"
role :app,    "192.168.1.154"
role :db,     "192.168.1.154", primary: true

set(:latest_release)  { fetch(:current_path) }
set(:release_path)    { fetch(:current_path) }
set(:current_release) { fetch(:current_path) }

set(:current_revision)  { capture("cd #{current_path}; git rev-parse --short HEAD").strip }
set(:latest_revision)   { capture("cd #{current_path}; git rev-parse --short HEAD").strip }
set(:previous_revision) { capture("cd #{current_path}; git rev-parse --short HEAD@{1}").strip }

default_environment["RAILS_ENV"] = 'production'

# Use our ruby-1.9.2-p318@my_site gemset
default_environment["PATH"]         = "--"
default_environment["GEM_HOME"]     = "--"
default_environment["GEM_PATH"]     = "--"
default_environment["RUBY_VERSION"] = "ruby-1.9.2-p318"

default_run_options[:shell] = 'bash'

namespace :deploy do
  desc "Deploy your application"
  task :default do
    update
    restart
  end

  desc "Setup your git-based deployment app"
  task :setup, except: { no_release: true } do
    dirs = [deploy_to, shared_path]
    dirs += shared_children.map { |d| File.join(shared_path, d) }
    run "#{try_sudo} mkdir -p #{dirs.join(' ')} && #{try_sudo} chmod g+w #{dirs.join(' ')}"
    run "git clone #{repository} #{current_path}"
  end

  task :cold do
    update
    migrate
  end

  task :update do
    transaction do
      update_code
    end
  end

  desc "Update the deployed code."
  task :update_code, except: { no_release: true } do
    run "cd #{current_path}; git fetch origin; git reset --hard #{branch}"
    finalize_update
  end

  desc "Update the database (overwritten to avoid symlink)"
  task :migrations do
    transaction do
      update_code
    end
    migrate
    restart
  end

  task :finalize_update, except: { no_release: true } do
    run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)

    # mkdir -p is making sure that the directories are there for some SCM's that don't
    # save empty folders
    run <<-CMD
      rm -rf #{latest_release}/log #{latest_release}/public/system #{latest_release}/tmp/pids &&
      mkdir -p #{latest_release}/public &&
      mkdir -p #{latest_release}/tmp &&
      ln -s #{shared_path}/log #{latest_release}/log &&
      ln -s #{shared_path}/system #{latest_release}/public/system &&
      ln -s #{shared_path}/pids #{latest_release}/tmp/pids &&
      ln -sf #{shared_path}/config/database.yml #{latest_release}/config/database.yml
    CMD

    #precompile the assets
    run "cd #{latest_release}; bundle exec rake assets:precompile"

    if fetch(:normalize_asset_timestamps, true)
      stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
      asset_paths = fetch(:public_children, %w(images stylesheets javascripts)).map { |p| "#{latest_release}/public/#{p}" }.join(" ")
      run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", env: { "TZ" => "UTC" }
    end
  end

  desc "Zero-downtime restart of Unicorn"
  task :restart, except: { no_release: true } do
    run "kill -s USR2 `cat /tmp/unicorn.my_site.pid`"
  end

  desc "Start unicorn"
  task :start, except: { no_release: true } do
    run "cd #{current_path} ; bundle exec unicorn_rails -c config/unicorn.rb -D"
  end

  desc "Stop unicorn"
  task :stop, except: { no_release: true } do
    run "kill -s QUIT `cat /tmp/unicorn.my_site.pid`"
  end  

  namespace :rollback do
    desc "Moves the repo back to the previous version of HEAD"
    task :repo, except: { no_release: true } do
      set :branch, "HEAD@{1}"
      deploy.default
    end

    desc "Rewrite reflog so HEAD@{1} will continue to point to at the next previous release."
    task :cleanup, except: { no_release: true } do
      run "cd #{current_path}; git reflog delete --rewrite HEAD@{1}; git reflog delete --rewrite HEAD@{1}"
    end

    desc "Rolls back to the previously deployed version."
    task :default do
      rollback.repo
      rollback.cleanup
    end
  end
end

def run_rake(cmd)
  run "cd #{current_path}; #{rake} #{cmd}"
end

Now there is one little thing you'll need to do. I like to run my apps, even on the server, to use their own gemset. This keeps everything clean and isolated. Login to the deployer account and create your gemset. Next run rvm info and fill the PATH, GEM_HOME and GEM_PATH variables accordingly.

Place the database configuration on the server in the shared directory.

# /home/deployer/apps/projectname.com/shared/config/database.yml 
production:
  adapter: postgresql
  encoding: unicode
  database: projectname_production
  pool: 5
  username: projectname_production
  password: password

Modify the server to support password-based SSH logins by editing /etc/ssh/sshd_config and setting:

PasswordAuthentication yes

Then restart ssh:

/etc/init.d/ssh restart

Set up a deploy key for the deployer user on your git repository. As deployer run the following:

ssh-keygen

Then:

cat ~/.ssh/id_rsa.pub

Push that as a deploy key to the project on github.

SSH into github as deployer from the server, to let this box validate that the host is valid to connect to:

ssh git@github.com

Then run a capistrano setup for the project:

cap deploy:setup

You also might have to go onto the server and, as deployer, run:

mkdir ~/apps/projectname.com/shared/pids

Deployments

Whenever you have a new feature developed in a feature branch, this is the process of deploying it:

Merge feature_branch into master
Run your tests to make sure everything is dandy.
Push master
Run `cap deploy`

Josh Adams is a developer and architect with over eleven years of professional experience building production-quality software and managing projects. Josh is isotope|eleven's lead architect, and is responsible for overseeing architectural decisions and translating customer requirements into working software. Josh graduated from the University of Alabama at Birmingham (UAB) with Bachelor of Science degrees in both Mathematics and Philosophy. He runs the ElixirSips screencast series, teaching hundreds of developers Elixir. He also occasionally provides Technical Review for Apress Publishing, specifically regarding Arduino microprocessors. When he's not working, Josh enjoys spending time with his family.