Ruby (Rails), Java, JavaScript, PostgreSQL, Cloud, Ubuntu :)

Desplegando Ruby on Rails en VPS (Unicorn + Nginx + Capistrano) con Ubuntu Server

Hoy abordaremos como desplegar una aplicación hecha en Ruby on Rails en un VPS. En mi caso usando Digital Ocean como proveedor. Comencemos:

Primero accedemos vía SSH a nuestro servidor, si aún no a has configurado y asegurado el acceso SSH a tu VPS puedes visitar un post donde explico como hacerlo. Si ya tienes todo listo entonces procedemos.

Configuraciones en el Servidor

 ssh -p numero-de-puerto nombre-de-usuario@123.123.123.123 
Si no lo hemos hecho aún, actualizamos los paquetes e instalamos curl
sudo apt-get update
sudo apt-get install curl
Instalamos la última versión estable de la RVM (Ruby Version Manager)
curl -L get.rvm.io | bash -s stable
Cargamos la RVM
source ~/.rvm/scripts/rvm
Instalamos las dependencias de la RVM
rvm requirements
Instalamos Ruby 2.0.0 (o bien podemos instalar la 2.1.0)
rvm install 2.0.0
Usamos la version 2.0.0 por defecto en RVM
rvm use 2.0.0 --default
Intalamos la última versión de rubygems en caso que RVM no la tenga aún
rvm rubygems current
Instalamos la gema de Rails, junto a Git, Bundler y NodeJS (necesario para las compilaciones de assets)
gem install rails --no-ri --no-rdoc
sudo apt-get install git-core
gem install bundler
**En caso que necesitemos instalar una versión especifica de Rails y no la más reciente (lo cual es lo recomendado) podemos hacerlo indicando que versión queremos.
gem install rails -v 3.2.14
Instalamos nuestro motor de base de datos, yo uso y recomiendo PostgreSQL, así que los siguientes pasos serán para instalarlo y configurarlo con Rails.
sudo apt-get install postgresql postgresql-server-dev-9.1
gem install pg -- --with-pg-config=/usr/bin/pg_config
Creamos un nuevo usuario en postgres (cambiar los campos correspondientes)
sudo -u postgres psql
create user nuevo-usuario with password 'contrasenia';
alter role nuevo-usuario superuser createrole createdb replication;
create database nombre_del_proyecto_production owner nuevo-usuario;
Instalamos Nginx
sudo apt-get install nginx
nginx -h
cat /etc/init.d/nginx
/etc/init.d/nginx -h
sudo service nginx start
cd /etc/nginx

Configuración Local

Comenzamos agregando Unicorn al Gemfile
gem 'unicorn'
Ahora creamos los siguientes archivos en el directorio /config * unicorn.rb * unicorn_init.sh * nginx.conf
Una vez creada, asignamos permisos de ejecución a unicorn_init.sh
chmod +x config/unicorn_init.sh
Agregamos la configuración de Nginx el archivo que hemos creado en el directorio /conf llamado nginx.conf (cambia nombre_de_proyecto y nombre_de_usuario con tus datos, el nombre del proyecto correspondería al nombre que le hayas asignado a tu aplicación de rails y el nombre de usuario es con el que accedes al servidor y por ende tiene asignado un directorio en la carpeta /home/nombre_de_usuario...)
upstream unicorn {
  server unix:/tmp/unicorn.nombre_de_proyecto.sock fail_timeout=0;
}

server {
  listen 80 default_server deferred;
  # server_name example.com;
  root /home/nombre_de_usuario/apps/nombre_de_proyecto/current/public;

  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 20M;
  keepalive_timeout 10;
}
Ahora editamos el archivo conf/unicorn.rb (recuerda cambiar nombre_de_proyecto y nombre_de_usuario conforme a tus datos). También puedes editar el número de workers que deseas que tenga tu aplicación, de momento solo he incluido dos.
root = "/home/nombre_de_usuario/apps/nombre_de_proyecto/current"
working_directory root
pid "#{root}/tmp/pids/unicorn.pid"
stderr_path "#{root}/log/unicorn.log"
stdout_path "#{root}/log/unicorn.log"

listen "/tmp/unicorn.nombre_de_protecto.sock"
worker_processes 2
timeout 30

# Force the bundler gemfile environment variable to
# reference the capistrano "current" symlink
before_exec do |_|
  ENV["BUNDLE_GEMFILE"] = File.join(root, 'Gemfile')
end
Editamos el archivo config/unicorn_init.sh (nuevamente recuerda cambiar el nombre_de_usuario y nombre_de_proyecto)
#!/bin/sh
### 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: Manage unicorn server
# Description:       Start, stop, restart unicorn server for a specific application.
### END INIT INFO
set -e

# Feel free to change any of the following variables for your app:
TIMEOUT=${TIMEOUT-60}
APP_ROOT=/home/nombre_de_usuario/apps/nombre_de_proyecto/current
PID=$APP_ROOT/tmp/pids/unicorn.pid
CMD="cd $APP_ROOT; bundle exec unicorn -D -c $APP_ROOT/config/unicorn.rb -E production"
AS_USER=nombre_de_usuario
set -u

OLD_PIN="$PID.oldbin"

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

oldsig () {
  test -s $OLD_PIN && kill -$1 `cat $OLD_PIN`
}

run () {
  if [ "$(id -un)" = "$AS_USER" ]; then
    eval $1
  else
    su -c "$1" - $AS_USER
  fi
}

case "$1" in
start)
  sig 0 && echo >&2 "Already running" && exit 0
  run "$CMD"
  ;;
stop)
  sig QUIT && exit 0
  echo >&2 "Not running"
  ;;
force-stop)
  sig TERM && exit 0
  echo >&2 "Not running"
  ;;
restart|reload)
  sig HUP && echo reloaded OK && exit 0
  echo >&2 "Couldn't reload, starting '$CMD' instead"
  run "$CMD"
  ;;
upgrade)
  if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
  then
    n=$TIMEOUT
    while test -s $OLD_PIN && test $n -ge 0
    do
      printf '.' && sleep 1 && n=$(( $n - 1 ))
    done
    echo

    if test $n -lt 0 && test -s $OLD_PIN
    then
      echo >&2 "$OLD_PIN still exists after $TIMEOUT seconds"
      exit 1
    fi
    exit 0
  fi
  echo >&2 "Couldn't upgrade, starting '$CMD' instead"
  run "$CMD"
  ;;
reopen-logs)
  sig USR1
  ;;
*)
  echo >&2 "Usage: $0 "
  exit 1
  ;;
esac

Configuración de Capistrano

Agregamos Capistrano y Capistrano RVM al Gemfile
gem 'capistrano'
gem 'rvm-capistrano'
Despues de haber ejecutado bundle install para instalar las gemas correspondientes ejecutamos el siguiente comando en el directorio principal de nuestra aplicación
capify .
Editamos (creamos en caso que no lo tengamos) el archivo deploy.rb en el directorio /config y agregamos la siguiente receta(recuerda sustituir los valores correspondientes conforme a los tu configuración):
require "bundler/capistrano"
require "rvm/capistrano"

server "123.123.123.123", :web, :app, :db, primary: true

set :application, "nombre_del_proyecto"
set :user, "nombre de usuario"
set :port, 22 #sustituye con el puerto que usas
set :deploy_to, "/home/#{user}/apps/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false

set :scm, "git"
set :repository, "git@github.com:username/#{application}.git" #o simplemente copia y pega la url del repositorio en el servicio que uses
set :branch, "master"


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

after "deploy", "deploy:cleanup" # keep only the last 5 releases

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

  task :setup_config, roles: :app do
    sudo "ln -nfs #{current_path}/config/nginx.conf /etc/nginx/sites-enabled/#{application}"
    sudo "ln -nfs #{current_path}/config/unicorn_init.sh /etc/init.d/unicorn_#{application}"
    run "mkdir -p #{shared_path}/config"
    put File.read("config/database.example.yml"), "#{shared_path}/config/database.yml"
    puts "Now edit the config files in #{shared_path}."
  end
  after "deploy:setup", "deploy:setup_config"

  task :symlink_config, roles: :app do
    run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
  end
  after "deploy:finalize_update", "deploy:symlink_config"

  desc "Make sure local git is in sync with remote."
  task :check_revision, roles: :web do
    unless `git rev-parse HEAD` == `git rev-parse origin/master`
      puts "WARNING: HEAD is not the same as origin/master"
      puts "Run `git push` to sync changes."
      exit
    end
  end
  before "deploy", "deploy:check_revision"
end
Ahora edita el archivo Capfile creado en el directorio principal de tu aplicación y verifica que esté de la siguiente manera:
load 'deploy'
load 'deploy/assets'
load 'config/deploy'

Despliegue

cap deploy:setup
Edita en el servidor /home/nombre-de-usuario/apps/nombre-de-proyecto/shared/config/database.yml conforme a los datos de conexión en el servidor remoto, en mi caso usando PostgreSQL para el entorno de producción quedaría de la siguiente manera.
production:
  adapter: postgresql
  encoding: unicode
  host: localhost
  database: nombre_de_bd_production
  pool: 5
  username: nombre_usuario_en_postgres
  password: 'ontrasenia'

Luego de haber ejecutado las configuración inicial y asumiendo que todo esta bien...
cap deploy:cold
Luego del deploy:cold
sudo rm /etc/nginx/sites-enabled/default
sudo service nginx restart
sudo update-rc.d -f unicorn_nombre_del_proyecto defaults
Por último para cada cambio nuevo que hagamos, hacemos push desde nuestro repositorio local y luego el deploy usando capistrano
git push origin master
cap deploy
Eso es todo, si tienes dudas, puedes contactarme o dejar tus comentarios.