Author: Sergey Kuznetsov kuznecov.sg@gmail.com

Published: April 2013

License: CC BY-SA (Creative Commons Attribution-ShareAlike)

Special note: This tutorial is based on capistrano 2.x, not 3.x

24.04.2013: updated nginx config to serve pre-gzipped assets static files and to set proper headers for them.

05.06.2013: updated commands for adding init.d script to autostart (thanks to Andrey Samsonov)

27.06.2013: small update to /etc/init.d unicorn script

04.09.2013: content of Capfile - we should explicitly load assets cap-tasks

22.09.2013: added section ‘logrotate config’

Buzz-words:

  • Ubuntu 12.04 LTS
  • System-wide RVM
  • Postgresql 9.1
  • Nginx
  • Ruby 1.9.3
  • Rails 3.2
  • Capistrano 2.15.5

Time from start to complete is about 1-1.5 hours.

user@server is a prefix for commands runned on the server side by user, dev on opposite on a developers machine.

OS installation

I will not describe how to install your system (it’s pretty much straight forward), but I have to point out on the next thing:

Do not use encryption for home folders (or you will have some kind of problems with ssh-keys).

Server updates right after OS installation

dev:~ $ ssh server
user@server:~$ sudo -i
root@server:~# apt-get update
root@server:~# apt-get dist-upgrade
root@server:~# shutdown -r now

SSH with public keys

dev:~ $ scp .ssh/id_dsa.pub server:~
dev:~ $ ssh server
Password: ...
user@server:~$ mkdir ~/.ssh
user@server:~$ chmod 700 ~/.ssh
user@server:~$ mv id_dsa.pub ~/.ssh/authorized_keys
user@server:~$ chmod 600 ~/.ssh/authorized_keys

The next time you log in to server it will not ask your password.

RVM install

I prefer to use system-wide installation of RVM.

Something got there https://www.digitalocean.com/community/articles/how-to-install-ruby-on-rails-on-ubuntu-12-04-lts-precise-pangolin-with-rvm

dev:~ $ ssh server
user@server:~$ sudo apt-get -y install curl build-essential openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev libgdbm-dev ncurses-dev automake libtool bison subversion pkg-config libffi-dev
user@server:~$ \curl -L https://get.rvm.io | sudo bash -s stable
user@server:~$ sudo usermod -a -G rvm user
user@server:~$ sudo usermod -a -G rvm root
user@server:~$ exit
dev:~ $ ssh server
user@server:~$ rvm install 1.9.3
user@server:~$ rvm use 1.9.3 --default

Node.js install

Something got there https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager

We need node.js for asset compilation purposes (there are other options, but node.js is much more quickiest way to compile assets).

user@server:~$ sudo apt-get install python-software-properties python
user@server:~$ sudo add-apt-repository ppa:chris-lea/node.js
user@server:~$ sudo apt-get update
user@server:~$ sudo apt-get install nodejs

Postgresql install

user@server:~$ sudo apt-get -y install postgresql libpq-dev

Nginx config

user@server:~$ sudo apt-get install nginx
user@server:~$ sudo vim /etc/nginx/sites-available/com.example

Contents of /etc/nginx/sites-available/com.example:

upstream com_example_unicorn {
    server unix:/var/www/com.example/shared/system/unicorn.sock fail_timeout=0;
}

server {
    listen 80;
    server_name example.com;

    access_log /var/log/nginx/com.example-access.log;
    error_log  /var/log/nginx/com.example-error.log;

    keepalive_timeout 5;

    root /var/www/com.example/current/public;

    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://com_example_unicorn;
    }

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

    error_page 500 502 503 504 /500.html;
    location = /500.html {
        root /var/www/com.example/current/public;
    }
}
user@server:~$ sudo ln -s /etc/nginx/sites-available/com.example /etc/nginx/sites-enabled/com.example
user@server:~$ sudo vim /etc/nginx/nginx.conf

Uncomment this line in /etc/nginx/nginx.conf:

#server_names_hash_bucket_size 64;

Now we able to restart nginx:

user@server:~$ sudo /etc/init.d/nginx restart

init.d shell script

We must be able to start/stop/restart our Rails application in easy way, and it must autostart after system reboot (like other daemons).

user@server:~$ sudo vim /etc/init.d/unicorn_com.example

Contents of /etc/init.d/unicorn_com.example (EUSER must be the same as main server user):

#!/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

# Add to auto-start on server start
# sudo update-rc.d unicorn_com.example defaults
# Remove from auto-start
# sudo update-rc.d -f unicorn_com.example remove

RVM_ENV="ruby-1.9.3-p392@com-example"
APP_NAME="com.example"
APP_PATH="/var/www/$APP_NAME"
EUSER="user"

RVM="source /usr/local/rvm/environments/$RVM_ENV"

CMD="$RVM && cd $APP_PATH/current && bundle exec unicorn"
CMD_OPTS="-c $APP_PATH/current/config/unicorn.rb -E production -D"
PID="$APP_PATH/shared/pids/unicorn.pid"

NAME=`basename $0`
DESC="Unicorn app for $APP_NAME"

case "$1" in
  start)
    echo -n "Starting $DESC: "
    su - $EUSER -c "$CMD $CMD_OPTS"
    echo "$NAME."
  ;;

  stop)
    echo -n "Stopping $DESC: "
    if [ -f $PID ] && [ -e /proc/$(cat $PID) ]
    then
      kill -QUIT `cat $PID`
      echo "$NAME."
    else
      echo "Unable to get $PID file, or process is down"
    fi
  ;;

  restart)
    echo -n "Restarting $DESC: "
    if [ -f $PID ] && [ -e /proc/$(cat $PID) ]
    then
      kill -USR2 `cat $PID`
    else
      su - $EUSER -c "$CMD $CMD_OPTS"
    fi
    echo "$NAME."
  ;;

  *)
    echo "Usage: $NAME {start|stop|restart}" >&2
    exit 1
  ;;
esac

exit 0

Then run this commands:

user@server:~$ sudo chmod 755 /etc/init.d/unicorn_com.example
user@server:~$ sudo update-rc.d unicorn_com.example defaults
user@server:~$ sudo visudo

Add this line to the end of sudoers file:

user ALL=(ALL) NOPASSWD: /etc/init.d/unicorn_com.example

Save it and close.

Unicorn config

Here is an example of unicorn.rb config:

APP_PATH = '/var/www/com.example'

rails_env = ENV['RAILS_ENV'] || 'development'

worker_processes 5

working_directory APP_PATH + '/current'

preload_app true

timeout 60

listen APP_PATH + '/shared/system/unicorn.sock', backlog: 64
pid APP_PATH + '/shared/pids/unicorn.pid'
stderr_path APP_PATH + '/shared/log/unicorn.stderr.log'
stdout_path APP_PATH + '/shared/log/unicorn.stdout.log'

before_exec do |server|
 ENV['BUNDLE_GEMFILE'] = APP_PATH + '/current/Gemfile'
end

before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!

  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
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

Almost done. It’s time to deploy our Rails application.

Capistrano

Ensure that you have included capistrano gem to your Gemfile:

group :development do
  # ...
  gem 'capistrano', '~> 2.15.5'
  gem 'capistrano_colors'
  gem 'rvm-capistrano'
  # ...
end

Capify project and update deploy.rb as you need:

dev:~/com.example $ capify .
dev:~/com.example $ vim config/deploy.rb

Content of ~/com.example/config/deploy.rb:

require 'bundler/capistrano'
require 'rvm/capistrano'

set :rvm_type,        :system
set :rvm_ruby_string, 'ruby-1.9.3-p392@com-example'

set  :application, 'com.example'

set :scm, :git
set :repository,  'git@github.com:user/com-example.git'
set :branch,      'master'

set :use_sudo,     false
set :user,         'user' # username on the server
set :deploy_to,     "/var/www/#{application}"
set :keep_releases, 5
set :ssh_options,   { forward_agent: true }

server 'example.com', :app, :web, :db, primary: true

# to use new assets approach
set :normalize_asset_timestamps, false

# unicorn related
set :unicorn_conf, "#{deploy_to}/current/config/unicorn.rb"
set :unicorn_pid,  "#{deploy_to}/shared/pids/unicorn.pid"

namespace :deploy do
  after 'deploy', 'deploy:cleanup'

  desc 'Zero-downtime restart of Unicorn'
  task :restart do
    run "sudo /etc/init.d/unicorn_#{application} restart"
  end

  task :start do
    run "sudo /etc/init.d/unicorn_#{application} start"
  end

  task :stop do
    run "sudo /etc/init.d/unicorn_#{application} stop"
  end
end

We need to uncomment line with loading assets tasks in a ~/com.example/Capfile, so it will be like this one:

load 'deploy'
load 'deploy/assets'
load 'config/deploy'

Predeploying

dev:~/com.example $ ssh server
user@server:~$ sudo mkdir -p /var/www/com.example
user@server:~$ sudo chown user:user /var/www/com.example
user@server:~$ rvm use 1.9.3-p392@com-example --create

Database

Here is example of config/database.yml file:

development:
  adapter: postgresql
  encoding: unicode
  database: com_example_development
  pool: 5

test:
  adapter: postgresql
  encoding: unicode
  database: com_example_test
  pool: 5

production:
  adapter: postgresql
  encoding: unicode
  database: com_example_production
  host: 127.0.0.1
  pool: 5
  username: user
  password: myPassword

Use your own production database name from database.yml. user in CREATE USER must be the same as your main user from server.

user@server:~$ sudo -i
root@server:~# su - postgres
postgres@server:~$ psql template1
template1=# CREATE USER user WITH PASSWORD 'myPassword';
template1=# CREATE DATABASE com_example_production;
template1=# GRANT ALL PRIVILEGES ON DATABASE com_example_production TO user;
template1=# \q
postgres@server:~$ exit
root@server:~# exit

Manually confirm repository key

dev:~/com.example $ ssh server
user@server:~$ git clone git@github.com:user/com-example.git
Cloning into 'com-example'...
The authenticity of host 'github.com (207.223.240.181)' can't be established.
RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40.
Are you sure you want to continue connecting (yes/no)? yes

Run migrations (in the future you’ll be able to run migrations by cap deploy:migrate command from development machine)

user@server:~$ cd com-example
user@server:~/com-example$ rvm use 1.9.3-p392@com-example
user@server:~/com-example$ bundle install
user@server:~/com-example$ RAILS_ENV=production rake db:migrate

Deploying

dev:~/com.example $ cap deploy:setup
dev:~/com.example $ cap deploy

We are done. Now you are able to use the next workflow:

dev:~/com.example $ # ... some changes to project
dev:~/com.example $ git add .
dev:~/com.example $ git commit -am "Commit message"
dev:~/com.example $ git push
dev:~/com.example $ cap deploy

logrotate config

We should be ready for incoming traffic. And we should aware of huge accessing log files of our app. So we can config a logrotate program with some simple actions. Here is the steps:

user@server:~$ sudo vim /etc/logrotate.d/unicorn_com.example

Here is the sample content of that file:

/var/www/com.example/shared/log/*.log {
  weekly
  missingok
  rotate 30
  compress
  delaycompress
  notifempty
  copytruncate
  size=100M
}

That’s all!

Now logrotate which runs on a daily basis, will rotate our app log-files if they become bigger than 100Mb. Description of other options you can find here http://linuxcommand.org/man_pages/logrotate8.html