Dreamisle.net Blog

Java, Rails, Security and so many ...

Applications Rails en Production Avec Nginx Et Unicorn Et Déployée Avec Capistrano

| Comments

Un peu d’infra aujourd’hui! Tout d’abord un grand merci à Bertrand Paquet de m’avoir formé à ces outils.


Pour faire tourner une application rails en production, on utilise souvent le duo passenger/apache. Personnellement j’ai lâché apache depuis longtemps pour Nginx. On ne va pas rentrer dans le débat du pourquoi mais essentiellement car Nginx est plus léger et plus performant.

Du coup pour mes applications rails j’utilisais le module nginx passenger. Sauf que celui ci est plein de bugs et très gourmand en ressources. La solution : unicorn.

Unicorn est un serveur rails léger pouvant être en écoute sur une socket unix. L’idée est donc de faire en sorte que Nginx et Unicorn communiquent à travers une socket unix. Un autre avantage du duo unicorn/nginx est qu’il permet d’heberger des applications rails sur autre chose que la racine d’un nom de domaine (par exemple dreamisle.net/monappli), hors avec passenger c’est très compliqué à faire.

Mais voyons un peu un exemple, admettons que je souhaite héberger une application Rails sur /monappli dans sur l’url mesapplis.dreamisle.net.

Nginx

Voyons déjà la configuration Nginx

upstream unicorn_upstream {
        server      unix:/home/leakim/ruby/monappli/shared/unicorn.sock fail_timeout=0;
}

server {
        access_log  /var/log/nginx/rails-access.log;
        error_log  /var/log/nginx/rails-access.log;
        server_name mesapplis.dreamisle.net;
        root  /home/leakim/ruby/dl/shared/www;
        location /monappli {
                proxy_set_header Host $http_host;
                if (!-f $request_filename) {
                        proxy_pass http://unicorn_upstream;
                        break;
                }
        }

        error_page 401 403 /401.html;

        error_page 404 /404.html;
        error_page 500 501 502 503 504 505 /500.html;

}

La partie interessante se situe dans le location /monappli. L’idée ici est que si nginx ne trouve pas le fichier demandé, il forward la requête sur la socket unix. la déclaration de la socket unix en haut du fichier de configuration est tout ce qu’il y a de plus classique.

Unicorn

Site officiel d’unicorn :

Unicorn est en fait une commande shell pour lancer un serveur rails que l’on configure via un script ruby. Configurons maintenant Unicorn.

Script de configuration unicorn

Celui-ci se configure via un script ruby. Voici un exemple de configuration.

worker_processes 3

app_directory = '/home/leakim/ruby/monappli'

working_directory "#{app_directory}/current"

listen "unix:#{app_directory}/shared/unicorn.sock", :backlog => 2048

timeout 600

preload_app true

pid "#{app_directory}/shared/pids/unicorn.pid"

stderr_path "#{app_directory}/shared/log/unicorn.stderr.log"
stdout_path "#{app_directory}/shared/log/unicorn.stdout.log"

if GC.respond_to?(:copy_on_write_friendly=)
  GC.copy_on_write_friendly = true
end

before_fork do |server, worker|
  old_pid = "#{app_directory}/shared/pids/unicorn.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
  # the following is 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
end

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

Config.ru

Il vous faut aussi compléter le fichier config.ru à la racine de votre application avec quelque chose de similaire :

# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment',  __FILE__)

map ActionController::Base.config.relative_url_root || "/" do
    run Monappli::Application
end

Script de lancement d’unicorn

Notez le RAILS_RELATIVE_ROOT_URL qui indique à unicorn l’url relative utilisée.

#! /bin/sh

### 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 unicorn web server
# Description:       starts unicorn web server
### END INIT INFO

NAME="unicorn"
USER="leakim"
DAEMON="/home/$USER/.rvm/bin/rvm-shell"
APP_DIRECTORY="/home/leakim/ruby/monappli"
PID_FILE="$APP_DIRECTORY/shared/pids/unicorn.pid"
CONFIG_FILE="$APP_DIRECTORY/shared/unicorn.conf.rb"
UNICORN_CMD="unicorn_rails"

export RAILS_RELATIVE_URL_ROOT="/monappli"

test -x $DAEMON || exit 4
test -d "$APP_DIRECTORY/current" || exit 5
test -f $CONFIG_FILE || exit 6

CMD="cd $APP_DIRECTORY/current && source .rvmrc && bundle exec $UNICORN_CMD -E production -D -c $CONFIG_FILE"

set -e

. /lib/lsb/init-functions

kill_unicorn() {
  SIGNAL=$1

    if [ ! "$PID_FILE" = "" ]; then
        if [ -f $PID_FILE ]; then
            kill $SIGNAL `cat $PID_FILE` || true
       fi
    fi
}

case "$1" in
   start)
    echo -n "Starting $NAME: "
    start-stop-daemon -c $USER --start --exec $DAEMON -- -c "$CMD" || exit 1
    echo "$NAME."
    ;;
  stop)
    echo -n "Stopping $NAME: "
    kill_unicorn
    echo "$NAME."
    ;;
  graceful_restart)
    echo -n "Graceful restarting $DESC: "
    kill_unicorn "-USR2"
    echo "$NAME."
    ;;
  restart)
    echo -n "Restarting $NAME: "
    kill_unicorn
    sleep 1
  start-stop-daemon -c $USER --start --exec $DAEMON -- -c "$CMD" || exit 1
    echo "$NAME."
    ;;
  status)
    status_of_proc -p $PID_FILE $UNICORN_CMD $UNICORN_CMD && exit 0 || exit $?
    ;;
  *)
    echo "Usage: $NAME {start|stop|restart|status|graceful_restart}" >&2
    exit 1
    ;;
esac

exit 0

Capistrano

Capistrano est un outil de gestion de déploiement distribué. L’idée est de pouvoir depuis son poste local, taper “cap deploy” pour deployer l’application en production. Capistrano peut aussi être utilisé en mode multistage pour utiliser plusieurs serveurs/environnements différents pour déployer. Je vous invite à découvrir l’outil ici : Capistrano. Ce fichier nommé deploy.rb doit être placé dans le repertoire config/ de votre application rails. Il vous permettra une fois la gem install (gem install capistrano), de lancer en local “cap deploy” pour déployer.

Si vous n’utilisez pas RVM il faut bien entendu supprimer tous les appels à celui-ci.

set :ssh_options, { :forward_agent => true } # utilise la clef ssh local et utilise l'option ssh forward agent, ce qui permet par exemple à git de cloner avec votre clef local
set :default_shell, "PATH=$HOME/.rvm/bin:$PATH bash"

set :user, 'leakim'
set :rails_relative_url_root, "/monappli"
set :deploy_to, "/home/leakim/ruby/monappli/"
set :use_sudo, false

server "monserver.net", :web, :app, :db

set :scm, :git
set :application, "monappli"
set :repository,  "git@github.com:user/repo.git"
set :deploy_via, :remote_cache

set :branch, 'master'

namespace :deploy do
  
  task :start do
    run "/etc/init.d/unicorn start"
  end
  
  task :stop do
    run "/etc/init.d/unicorn stop"
  end
  
  task :restart, :roles => :app do
    run "/etc/init.d/unicorn restart"
  end

end

after 'deploy:finalize_update', 'rvm', 'bundle', 'db_config', 'db_migrate', 'precompile_assets'

task :rvm, :roles => :app do
  run "cd #{release_path} && source .rvmrc"
end

task :bundle, :roles => :app do
  run "cd #{release_path} && source .rvmrc && bundle --without development"
end

task :precompile_assets, :roles => :app do
  prefix = rails_relative_url_root && rails_relative_url_root.size > 0 ? "RAILS_RELATIVE_URL_ROOT=#{rails_relative_url_root}" : ""
  run "cd #{release_path} && source .rvmrc && #{prefix} RAILS_ENV=production rake assets:precompile --trace"
end

task :db_config, :roles => :app do
  run "cd #{release_path} && ln -s #{shared_path}/database.yml config/database.yml && ln -s #{shared_path}/configuration.yml config/configuration.yml"
end

task :db_migrate, :roles => :db do
  run "cd #{release_path} && source .rvmrc && RAILS_ENV=production rake db:migrate --trace"
end

Comments