Целью данной заметки я ставлю в подробностях описать организацию сервера для Rails приложений в самой популярной на данный момент связке: rvm + Rails + Nginx + Unicorn. К написанию статьи побудило отсутствие полной пошаговой документации по этой связке, понятной не только ядреным профессионалам этой области. Далее я попытаюсь подробно, шаг за шагом, описать идеологически правильный процесс организации сервера для обслуживания нескольких Rails приложений (на примере одного) — если у вас есть абсолютная уверенность в том, что на подопытной машине никогда не будет работать более одного приложения — настройка может быть существенно короче и проще. Хочу предупредить, что тонкости, касающиеся работы приложения под высокой нагрузкой в статье не описываются, т.к. цель ставилась иная — заставить работать приложение в связке и сократить количество конфликтов с другими приложениями до минимума.SSH ключ
Прежде, чем использовать инструменты, перечисленные в заголовке, необходимо подготовить сервер на котором мы собираемся все организовать. Предположим, вы только что установили свежую Ubuntu 10.04 LTS на сервер (+ завели в процессе первого пользователя), и подняли там OpenSSH daemon. Все! Отныне сервер не должен быть прикасаем для рук, ног и других конечностей — работать с ним мы будем только удаленно, а для этого на своей рабочей машине следует выполнить:
где vasya — это имя пользователя на сервере, от имени и прав которого будет осуществляться деплой, а rails-production.example.com — это адрес или имя только что поднятого вами сервера. После ввода будет необходимо согласиться с добавлением хоста в список known hosts у вас на машине — ничего страшного — это нормально. И ввести Васин пароль. Это будет последним разом, когда вы будете вводить пароль Василия. Теперь доступ к серверу возможен по ssh ключу и ничего вводить не надо.ssh-copy-id vasya@rails-production.example.com
Казалось бы, это основы о которых и упоминать-то не стоит — но всегда есть определенный процент людей, имеющий альтернативную точку зрения на доступ к машине по 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 — от root (так называемая system wide install) и обычная — для простого пользователя. Так вот, опять я хочу испытать вашу веру — не устанавливайте от root. Не зря же мы создавали пользователя, ответственного за деплой. Поэтому зайдя на сервер под нашим Василием выполняем следующую последовательность команд:
Последняя команда должна выдать «rvm is a function» или аналог на русском языке. Если этого не произошло — стоит начать изучение отсюда и до момента — «пока оно не заработает».sudo apt-get install git-core curl #Это для того, чтобы заработала установка Rvm. curl -L https://get.rvm.io | bash -s stable --ruby type rvm | head -1
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 соответствующий гем:
и запускать Unicorn командойgem '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
Ну вот мы уже настроили все
После установки гема выполняем команду:group :development do gem "capistrano" gem "rvm-capistrano" end
и получаем почти пустой файл config/deploy.rb. Я же приведу пример файла под наши нужды:bundle exec capify .
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. Если у кого-то возникнут предложения по улучшению изложенного материала или конструктивные замечания буду рад прочесть и исправить.
