Pull to refresh

Связка rvm + Rails + Nginx + Unicorn или деплоим рельсы правильно

Reading time9 min
Views54K
Целью данной заметки я ставлю в подробностях описать организацию сервера для Rails приложений в самой популярной на данный момент связке: rvm + Rails + Nginx + Unicorn. К написанию статьи побудило отсутствие полной пошаговой документации по этой связке, понятной не только ядреным профессионалам этой области. Далее я попытаюсь подробно, шаг за шагом, описать идеологически правильный процесс организации сервера для обслуживания нескольких Rails приложений (на примере одного) — если у вас есть абсолютная уверенность в том, что на подопытной машине никогда не будет работать более одного приложения — настройка может быть существенно короче и проще. Хочу предупредить, что тонкости, касающиеся работы приложения под высокой нагрузкой в статье не описываются, т.к. цель ставилась иная — заставить работать приложение в связке и сократить количество конфликтов с другими приложениями до минимума.

SSH ключ

Прежде, чем использовать инструменты, перечисленные в заголовке, необходимо подготовить сервер на котором мы собираемся все организовать. Предположим, вы только что установили свежую Ubuntu 10.04 LTS на сервер (+ завели в процессе первого пользователя), и подняли там OpenSSH daemon. Все! Отныне сервер не должен быть прикасаем для рук, ног и других конечностей — работать с ним мы будем только удаленно, а для этого на своей рабочей машине следует выполнить:
ssh-copy-id vasya@rails-production.example.com
где vasya — это имя пользователя на сервере, от имени и прав которого будет осуществляться деплой, а rails-production.example.com — это адрес или имя только что поднятого вами сервера. После ввода будет необходимо согласиться с добавлением хоста в список known hosts у вас на машине — ничего страшного — это нормально. И ввести Васин пароль. Это будет последним разом, когда вы будете вводить пароль Василия. Теперь доступ к серверу возможен по ssh ключу и ничего вводить не надо.

Казалось бы, это основы о которых и упоминать-то не стоит — но всегда есть определенный процент людей, имеющий альтернативную точку зрения на доступ к машине по ssh ключам. Для них я могу посоветовать мазь от артрита суставов кисти, остальным же предлагаю просто поверить, что ssh ключи — это благо.

База данных

Здесь я лишь дам подсказки по установке нужных пакетов для двух самых популярных СУБД:
sudo apt-get install mysql-server mysql-client libmysqld-dev # MySQL 
sudo apt-get install postgresql postgresql-client postgresql-server-dev #Postgresql
Настройка СУБД на сервере — это тема отдельной статьи, поэтому предположим, что с этим вы можете справиться самостоятельно. Поэтому — тадааам! СУБД запущена и работает.

Rvm

Rvm — это средство управления версиями Ruby в системе, позволяющее создавать отдельные «окружения» из гемов, что в нашем случае не важно. Если рассмотреть концепции bundler и гемсетов Rvm, то может возникнуть чувство, что они созданы для одной и той же цели — изолировать окружение для работы конкретного приложения. Bundler — это замечательное средство разрешения зависимостей гемов, к тому же Rails 3 по умолчанию работает именно с ним. И вообще раз уж об этом зашла речь — я рекомендую использовать bundler для Rails 2.3.x, как это можно сделать описано здесь.

Rvm нам нужен только для того, чтобы без труда переключаться между разными версиями Ruby, и такая необходимость скорее всего возникнет на сервере, где одновременно будут крутиться приложения, написанные на разных версиях Ruby on Rails. У Rvm есть и свои противники. Нет и на самом деле — если вы абсолютно точно уверены, что на этой железке никогда не будет работать больше одной версии ruby, то будет правильнее установить какой-нибудь ree как системный интерпретатор ruby и гореть в аду потихонечкурадоваться жизни. Но реальность сурова, поэтому я просто рекомендую использовать rvm — это позволит держать все вещи в порядке.

Если вы уже ознакомились с документацией, то наверное заметили, что существует два способа установки Rvm — от root (так называемая system wide install) и обычная — для простого пользователя. Так вот, опять я хочу испытать вашу веру — не устанавливайте от root. Не зря же мы создавали пользователя, ответственного за деплой. Поэтому зайдя на сервер под нашим Василием выполняем следующую последовательность команд:

sudo apt-get install git-core curl #Это для того, чтобы заработала установка Rvm.

curl -L https://get.rvm.io | bash -s stable --ruby

type rvm | head -1
Последняя команда должна выдать «rvm is a function» или аналог на русском языке. Если этого не произошло — стоит начать изучение отсюда и до момента — «пока оно не заработает».

Ruby

Выбор версии руби зависит от того, какая версия рельс используется в приложении: так для 2.3.x предпочтительнее использовать Ruby Enterprise Edition, для 3.x рекомендуют использовать 1.9.3. В моем случае — это приложение на 2.3.x и ree соответственно. И если для установки ruby 1.9.3 ничего экстраординарного от системы не требуется, то для установки ree необходимо несколько системных пакетов:
sudo apt-get install build-essential bison openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-0 libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev ncurses-dev

rvm install ree-1.8.7-2011.03 # Устанавливаем ree

rvm ree # Указываем, какой интерпретатор Ruby использовать. Если вам так больше нравится - создайте себе гемсет вот так: rvm ree@myapp --create. Но с Bundler он становится попросту ненужным.

gem install bundler # Единственный gem, который мы поставим руками.

sudo mkdir -p /srv/myapp # Создаем директорию, в которой будет находиться наше приложение.

sudo chown -R vasya:vasya /srv/myapp # Передаем права на владение Василию (так как директория пуста, параметр -R можно пропустить).


Nginx

Раз уж мы будем использовать Unicorn, то без Nginx нам никак не обойтись — есть у Unicorn такая особенность — не может он работать с медленными клиентами. Есть, правда, его аналог, который может — Rainbows, но Nginx сам по себе исключительно полезен, хорош и прост в эксплуатации. Инструкции по установке вы можете найти на сайте автора этого замечательного сервера — Игоря Сысоева. Я лишь приведу здесь простой init скрипт для запуска nginx и nginx.conf:
#! /bin/sh
 
EXEC_PATH="/usr/local/nginx/sbin/nginx"

case "$1" in
    start)
        echo "Starting NginX"
        start-stop-daemon --start --exec $EXEC_PATH
        ;;
    stop)
        echo "Stopping NginX"
        start-stop-daemon --stop --exec $EXEC_PATH
        ;;
    restart)
        echo "Stopping NginX"
        start-stop-daemon --stop --exec $EXEC_PATH
        sleep 1
        echo "Starting NginX"
        start-stop-daemon --start --exec $EXEC_PATH
        ;;
    *)
        echo "Usage: {start|stop|restart}"
        exit 1
        ;;
esac
exit 0

worker_processes 1; # Более одного рабочего процесса обычно не требуется.
user vasya vasya; # Пользователь с правами которого запускается worker - он же пользователь, от которого осуществляется деплой.

pid /tmp/nginx.pid; # Задаем местоположение файла с идентификатором текущего мастер-процесса Nginx.
error_log /tmp/nginx.error.log; 

events {
    worker_connections 1024; # Стандартный показатель количества одновременно открытых соединений рабочего процесса.
    accept_mutex off; # Ну и раз уж воркер у нас один - отключаем.
}

http {
  # Дальше немного стандартных директив - если нет особого желания менять здесь что-то - не меняйте:
  include mime.types;
  default_type application/octet-stream;
  access_log /tmp/nginx.access.log combined;
  sendfile on;
  tcp_nopush on;
  tcp_nodelay off;
  gzip on;
  
  # Теперь самая сладкая часть. Далее происходит указание upstream сервера. Так как все происходит в рамках одной машины, слушать апстрим лучше через сокет.
  upstream myapp_server {
   server unix:/srv/myapp/shared/unicorn.sock fail_timeout=0; # Местоположение сокета должно совпадать с настройками файла config/unicorn.rb от корня вашего приложения.
  }

  server {
    listen 80 default deferred; # Опять же, если на одном и том же ip находится несколько серверов, то эта строка будет выглядеть как-то так myapp.mydomain.ru:80
    client_max_body_size 1G; # Максимальный размер тела запроса (а простым языком - ограничение на размер заливаемого на сервер файла).
    server_name myapp.mydomain.ru; # Имя сервера
    keepalive_timeout 5;
    root /srv/myapp/current/public; # Эта строка всегда должна указывать в директорию public Rails приложения. А current там потому что деплой происходит через Capistrano

    try_files $uri/index.html $uri.html $uri @myapp; # Имя переменной не важно - главное, чтобы в блоке location ниже было аналогичное
    
    location @myapp {
        proxy_pass http://myapp_server; # Часть после http:// должна полностью соответствовать имени в блоке upstream выше.
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
    }
    
    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /srv/myapp/current/public;
    }
  }
}

Стоит сразу предупредить, что когда Nginx одновременно обслуживает несколько приложений то блоки server { ... } стоит выносить в отдельные файлы в директории /usr/local/nginx/conf/vhosts а в nginx.conf писать include /usr/local/nginx/conf/vhosts/* — в примере этого не сделано для наглядности.

Unicorn

Unicorn можно установить для всей системы, но делать этого не следует — гораздо правильнее включить в Gemfile соответствующий гем:
gem 'unicorn'
и запускать Unicorn командой bundle exec. Кстати, рекомендация распространяется не только Unicorn, но и на любые исполняемые файлы, идущие вместе с гемами. Установка Unicorn в рамках конкретного приложения позволит вам без проблем завести сколько угодно приложений на одной машине.
Далее я приведу пример конфигурации сервера, которая обеспечивает так называемый zero downtime deploy. Итак, config/unicorn.rb:
deploy_to  = "/srv/myapp"
rails_root = "#{deploy_to}/current"
pid_file   = "#{deploy_to}/shared/pids/unicorn.pid"
socket_file= "#{deploy_to}/shared/unicorn.sock"
log_file   = "#{rails_root}/log/unicorn.log"
err_log    = "#{rails_root}/log/unicorn_error.log"
old_pid    = pid_file + '.oldbin'

timeout 30
worker_processes 4 # Здесь тоже в зависимости от нагрузки, погодных условий и текущей фазы луны
listen socket_file, :backlog => 1024
pid pid_file
stderr_path err_log
stdout_path log_file

preload_app true # Мастер процесс загружает приложение, перед тем, как плодить рабочие процессы.

GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=) # Решительно не уверен, что значит эта строка, но я решил ее оставить.

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = "#{rails_root}/Gemfile"
end

before_fork do |server, worker|
  # Перед тем, как создать первый рабочий процесс, мастер отсоединяется от базы.
  defined?(ActiveRecord::Base) and
  ActiveRecord::Base.connection.disconnect!

  # Ниже идет магия, связанная с 0 downtime deploy.
  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|
  # После того как рабочий процесс создан, он устанавливает соединение с базой.
  defined?(ActiveRecord::Base) and
  ActiveRecord::Base.establish_connection
end


Capistrano

Ну вот мы уже настроили все, что только можно базовые вещи, поэтому следует разобраться с Capistrano. Для начала стоит поместить гем capistrano в группу :development нашего Gemfile, а также поместить туда же rvm-capistrano для интеграции с rvm (для версий rvm >= 1.1.0):
group :development do
  gem "capistrano"
  gem "rvm-capistrano"
end
После установки гема выполняем команду:
bundle exec capify .
и получаем почти пустой файл config/deploy.rb. Я же приведу пример файла под наши нужды:
require 'rvm/capistrano' # Для работы rvm
require 'bundler/capistrano' # Для работы bundler. При изменении гемов bundler автоматически обновит все гемы на сервере, чтобы они в точности соответствовали гемам разработчика. 

set :application, "myapp"
set :rails_env, "production"
set :domain, "vasya@rails-production.example.com" # Это необходимо для деплоя через ssh. Именно ради этого я настоятельно советовал сразу же залить на сервер свой ключ, чтобы не вводить паролей.
set :deploy_to, "/srv/#{application}"
set :use_sudo, false
set :unicorn_conf, "#{deploy_to}/current/config/unicorn.rb"
set :unicorn_pid, "#{deploy_to}/shared/pids/unicorn.pid"

set :rvm_ruby_string, 'ree' # Это указание на то, какой Ruby интерпретатор мы будем использовать.

set :scm, :git # Используем git. Можно, конечно, использовать что-нибудь другое - svn, например, но общая рекомендация для всех кто не использует git - используйте git. 
set :repository,  "git@github.com:myprojects/myapp.git" # Путь до вашего репозитария. Кстати, забор кода с него происходит уже не от вас, а от сервера, поэтому стоит создать пару rsa ключей на сервере и добавить их в deployment keys в настройках репозитария.
set :branch, "master" # Ветка из которой будем тянуть код для деплоя.
set :deploy_via, :remote_cache # Указание на то, что стоит хранить кеш репозитария локально и с каждым деплоем лишь подтягивать произведенные изменения. Очень актуально для больших и тяжелых репозитариев.

role :web, domain
role :app, domain
role :db,  domain, :primary => true

before 'deploy:setup', 'rvm:install_rvm', 'rvm:install_ruby' # интеграция rvm с capistrano настолько хороша, что при выполнении cap deploy:setup установит себя и указанный в rvm_ruby_string руби.

after 'deploy:update_code', :roles => :app do
  # Здесь для примера вставлен только один конфиг с приватными данными - database.yml. Обычно для таких вещей создают папку /srv/myapp/shared/config и кладут файлы туда. При каждом деплое создаются ссылки на них в нужные места приложения.
  run "rm -f #{current_release}/config/database.yml"
  run "ln -s #{deploy_to}/shared/config/database.yml #{current_release}/config/database.yml"
end

# Далее идут правила для перезапуска unicorn. Их стоит просто принять на веру - они работают.
# В случае с Rails 3 приложениями стоит заменять bundle exec unicorn_rails на bundle exec unicorn
namespace :deploy do
  task :restart do
    run "if [ -f #{unicorn_pid} ] && [ -e /proc/$(cat #{unicorn_pid}) ]; then kill -USR2 `cat #{unicorn_pid}`; else cd #{deploy_to}/current && bundle exec unicorn_rails -c #{unicorn_conf} -E #{rails_env} -D; fi"
  end
  task :start do
    run "bundle exec unicorn_rails -c #{unicorn_conf} -E #{rails_env} -D"
  end
  task :stop do
    run "if [ -f #{unicorn_pid} ] && [ -e /proc/$(cat #{unicorn_pid}) ]; then kill -QUIT `cat #{unicorn_pid}`; fi"
  end
end


P.S. Если у кого-то возникнут предложения по улучшению изложенного материала или конструктивные замечания буду рад прочесть и исправить.
Tags:
Hubs:
Total votes 68: ↑63 and ↓5+58
Comments135

Articles