Настройка ruby 1.9.3 в облаке Hostpro

Друзья, сегодня мы хотим поделиться с вами интересным материалом, который был написан нашим клиентом. Ниже будет описана процедура установки Ruby 1.9.3 + Unicorn + Nginx + Mysql на сервер под управлением CentOS 5.x. Весь процесс установки происходил на сервере, который размещён в нашем облаке.
Это краткий конспект шагов, необходимых для установки и запуска нескольких одновременно работающих Ruby on Rails приложений на CloudVPS сервере Hostpro с чистым дистрибутивом CentOS 5.5.
Устанавливаемое ПО
- CentOS 5.5
- Mysql 5
- Nginx
- RVM
- Ruby 1.9.3
- Unicorn web server
- Rails 3.2
CentOS — операционная система, MySQL — базы данных, с ними вроде всё ясно.
Nginx + Unicorn — одна из самых популярных связок в Ruby-мире. Nginx встречает запросы и отдаёт статику (картинки, скрипты, css и прочее), а остальное передаёт Rails-приложению т.е. Unicorn’у. Это позволяет увеличить скорость обработки запросов и существенно снизить нагрузку с приложения. https://github.com/blog/517-unicorn.
RVM — Ruby Version Manager. Позволяет использовать разные версии Ruby и наборы gem-пакетов в Rails-приложениях. Например, одно из приложений всё ещё работает на Ruby 1.8.7 и использует старые версии gem’ов. Типичный пример — Redmine, который не хочется апгрейдить. Мы создаём под него свою конфигурацию Ruby 1.8.7 + пакеты и т.д. https://rvm.io
Создаём пользователя и настраиваем SSH-доступ
Уже в самом начале удобно задать для сервера алиас и прописать его в настройках ~/.ssh/config:
Host hostpro Hostname 194.28.86.211 # или Hostname onapp.my_domain.net
Такая запись позволяет впоследствии писать:
ssh hostpro # вместо [email protected]_domain.net
Итак, перед нами чистый дистрибутив CentOS с root-доступом:
ssh [email protected] Last login: Fri Jun 15 01:20:47 2012 from 91.218.89.42 [[email protected] ~]# cat /etc/redhat-release CentOS release 5.5 (Final) [[email protected] ~]# pwd /root
Добавляем пользователя:
useradd -m -G users wheel stanislaw
Задаём пароль:
passwd stanislaw cd /home/stanislaw
С локального компьютера копируем публичный ключ ssh на сервер, на локальном компьютере выполняем:
scp ~/.ssh/id_rsa.pub hostpro:/home/stanislaw
На сервере содержимое публичного ключа добавляем в перечень допустимых ключей (сервер чистый, поэтому, конечно, файл ~/.ssh/authorized_keys пока пуст):
[[email protected] ~]$ mkdir ~/.ssh [[email protected] ~]$ cat ~/id_rsa >> ~/.ssh/authorized_keys
Результат — теперь мы можем просто писать:
ssh hostpro
Вводим свой любимый пароль и попадаем в домашнюю директорию созданного пользователя.
Устанавливаем MySQL
Устанавливаем пакеты:
su yum install mysql mysql-devel mysql-server
Запускаем MySQL:
/etc/init.d/mysqld start
Задаём root-пароль:
/usr/bin/mysqladmin -u root password 'root-cool-pass'
Заходим в mysql-консоль, чтобы создать пользователя stanislaw:
mysql -u root -p mysql> GRANT ALL PRIVILEGES ON *.* TO 'stanislaw'@'localhost' IDENTIFIED BY 'mysql-cool-pass' WITH GRANT OPTION;
Впоследствии, конечно, желательно ограничить права пользователя stanislaw — разрешить только те базы, для которых ему действительно нужен доступ.
Добавляем MySQL в автозапуск (сервер по умолчанию работает на runlevel 3):
/sbin/chkconfig --add mysqld /sbin/chkconfig --level 3 mysqld on
Nginx
yum install nginx
После установки нужно сделать:
chmod 0751 /home/stanislaw # Если интересно, причина: http://stackoverflow.com/questions/6795350/nginx-403-forbidden-for-all-files
Добавляем nginx в авто-запуск:
sudo /sbin/chkconfig nginx on sudo /sbin/chkconfig --list nginx nginx 0:off 1:off 2:on 3:on 4:on 5:on 6:off
Git
в пакетах CentOS 5 git отсутствует.
Наиболее частое решение — использовать дополнительный репозиторий (https://webtatic.com/)
rpm -Uvh http://repo.webtatic.com/yum/centos/5/latest.rpm yum install --enablerepo=webtatic git-all
ImageMagick
Его обязательно потребует при установке gem rmagick, который используется практически в любом Rails-проекте. Понадобится свежая версия прямо с родного сайта, т.к. версия в репозитории CentOS 5 очень давняя:
wget http://www.imagemagick.org/download/linux/CentOS/i386/ImageMagick-6.7.7-7.i386.rpm wget http://www.imagemagick.org/download/linux/CentOS/i386/ImageMagick-devel-6.7.7-7.i386.rpm
Библиотеки, которые потребуются при сборке:
yum install libtool-ltdl lzma freetype-devel jasper-devel
И, наконец:
rpm -Uvh ImageMagick-6.7.7-7.i386.rpm rpm -Uvh ImageMagick-devel-6.7.7-7.i386.rpm
Ruby + RVM
RVM рекомендуется устанавливать в домашнюю директорию своего пользователя. Все действия (кроме установки пакетов с использованием yum) в этом разделе ведутся от имени пользователя, а не администратора!
Чтобы не возиться с SSL-сертификатами (после установки RVM это, конечно, лучше убрать):
echo insecure >> ~/.curlrc
Устанавливаем RVM:
curl -L https://get.rvm.io | bash -s stable
Смотрим, что нужно для установки Ruby, команда
rvm requirements
отобразит все необходимые зависимости, которые выглядят так:
yum install -y gcc-c++ patch readline readline-devel zlib zlib-devel libyaml-devel libffi-devel openssl-devel make bzip2 autoconf automake libtool bison
Дополнительные зависимости для разных гемов:
yum install libxml2 libxml2-devel libxslt libxslt-devel
Наконец, можно установить Ruby:
rvm install 1.9.3
И Bundler:
gem install bundler
Unicorn
gem install unicorn
Наверное, единственное, что стоит отметить про связку Nginx + Unicorn — это то, что между собой Unicorn и Nginx могут взаимодействовать как через порты, так и через сокеты. Второй способ предпочтителен и используется в конфигурации ниже: Unicorn слушает socket-файл; это означает, что со стороны портов Unicorn можно закрыть вообще, см. соответствующий комментарий в конфигурационном файле config/unicorn.rb ниже.
Предположим, что у нас есть два Rails-приложения: first_app и second_app.
Каждое приложение должно содержать конфигурационный файл config/unicorn.rb со следующим содержанием (пример дляfirst_app):
Конфигурация Unicorn (config/unicorn.rb)
#worker_processes 4 deploy_to = "/home/stanislaw/apps/first_app" 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" error_log_file = "#{rails_root}/log/unicorn_error.log" old_pid = pid_file + ".oldbin" working_directory rails_root listen socket_file, :backlog => 64 # listen 8080, :tcp_nopush => true # Слушать порт нет необходимости, потому что nginx будет передавать запросы через socket-файл. pid pid_file stderr_path error_log_file stdout_path log_file timeout 30 preload_app true GC.respond_to?(:copy_on_write_friendly=) and GC.copy_on_write_friendly = true before_exec do |server| ENV["BUNDLE_GEMFILE"] = "#{rails_root}/Gemfile" end before_fork do |server, worker| defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect! 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| # per-process listener ports for debugging/admin/migrations # addr = "127.0.0.1:#{9293 + worker.nr}" # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) # the following is *required* for Rails + "preload_app true", defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection # if preload_app is true, then you may also want to check and # restart any other shared sockets/descriptors such as Memcached, # and Redis. TokyoCabinet file handles are safe to reuse # between any number of forked children (assuming your kernel # correctly implements pread()/pwrite() system calls) end
Обратите особое внимание на путь, указанный в переменной pid_file — он понадобится нам для работы скрипта автозапуска/etc/init.d/unicorn, а путь из socket_file мы укажем в настройках Nginx.
Конфигурация Nginx (/etc/nginx/nginx.conf)
user nginx; worker_processes 1; error_log /var/log/nginx/error.log; #error_log /var/log/nginx/error.log notice; #error_log /var/log/nginx/error.log info; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; gzip on; gzip_http_version 1.0; gzip_proxied any; gzip_min_length 500; gzip_disable "MSIE [1-6]\."; gzip_types text/plain text/html text/xml text/css text/comma-separated-values text/javascript application/x-javascript application/atom+xml; include /etc/nginx/conf.d/*.conf; upstream first_app_server { server unix:/home/stanislaw/apps/first_app/shared/unicorn.sock fail_timeout=0; } upstream second_app_server { server unix:/home/stanislaw/apps/second_app/shared/unicorn.sock fail_timeout=0; } server { listen 80 default_server; client_max_body_size 4G; server_name first_app.com www.first_app.com; location /robots.txt { alias /usr/local/etc/nginx/robots.txt; } location ~ ^/assets/ { expires 1y; add_header Cache-Control public; add_header Last-Modified ""; add_header ETag ""; break; } keepalive_timeout 5; root /home/stanislaw/apps/first_app/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://first_app_server; } error_page 500 502 503 504 /500.html; location = /500.html { root /home/stanislaw/apps/first_app/current/public; } } server { listen 80 default_server; client_max_body_size 4G; server_name second_app.com www.second_app.com; location /robots.txt { alias /usr/local/etc/nginx/robots.txt; } location ~ ^/assets/ { expires 1y; add_header Cache-Control public; add_header Last-Modified ""; add_header ETag ""; break; } keepalive_timeout 5; root /home/stanislaw/apps/second_app/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://second_app_server; } error_page 500 502 503 504 /500.html; location = /500.html { root /home/stanislaw/apps/second_app/current/public; } } }
Конфигурация Capistrano (config/deploy.rb)
load 'deploy/assets' # require 'bundler/capistrano' # Важные строки для правильного включения RVM https://rvm.io//integration/capistrano/ set :rvm_ruby_string, '1.9.3' require 'rvm/capistrano' set :user, "stanislaw" set :application, "first_app" set :repository, "." set :hostpro_ip, "194.28.86.211" set :deploy_to, "/home/stanislaw/apps/first_app" set :scm, :none set :scm_verbose, :true set :deploy_via, :copy set :use_sudo, :false set :ssh_options, { :forward_agent => true, :keys => %w(/home/stanislaw/.ssh/id_rsa) } set :unicorn_conf, "#{deploy_to}/current/config/unicorn.rb" set :unicorn_pid, "#{deploy_to}/shared/pids/unicorn.pid" role :web, hostpro_ip # Your HTTP server, Apache/etc role :app, hostpro_ip # This may be the same as your `Web` server role :db, hostpro_ip, :primary => true # This is where Rails migrations will run # role :db, "your slave db-server here" before "deploy:assets:precompile" do run "cd #{latest_release}; bundle install --without=development test" end after "deploy:assets:precompile" do # run "cd #{latest_release}; bundle install --without=development test" end after 'deploy:update_code' do # run "cd #{latest_release}; RAILS_ENV=production rake assets:precompile" end namespace :deploy do task :restart do run "kill -USR2 `cat #{unicorn_pid}`" end task :start do run "cd #{deploy_to}/current; bundle exec unicorn_rails -c #{unicorn_conf} -E #{rails_env} -D" end task :stop do run "kill -QUIT `cat #{unicorn_pid}`" end end
Скрипт для авто-запуска /etc/init.d/unicorn (не забудьте сделать его исполняемым)
#!/bin/bash #chkconfig: 345 20 80 #description: Multiple unicorns startup script # init.d script for single or multiple unicorn installations. Expects at least one .conf # file in /etc/unicorn # # (2012.06.18) Modified by [email protected] http://github.com/stanislaw, as taken from http://jay.gooby.org/post/an-etcinitdunicorn-script-for-multiple-unicorn-installations: # * RVM-based environments (.rvmrc should go in RAILS_ROOT if it differs from default gemset) # * Replaced 'exit 0' with 'return'. It was breaking entire script after executing command (fx. stop) for the first file in /etc/unicorn/ # Modified by [email protected] http://github.com/jaygooby # based on http://gist.github.com/308216 by http://github.com/mguterl # ## A sample /etc/unicorn/my_app.conf ## ## RAILS_ENV=production ## RAILS_ROOT=/home/stanislaw/apps/my_app/current ## UNICORN_PID=/home/stanislaw/apps/my_app/shared/pids/unicorn.pid # This configures a unicorn master for your app at /home/stanislaw/apps/my_app/current running in production mode. It will read config/unicorn.rb for further set up. # # You should ensure different ports or sockets are set in each config/unicorn.rb if you are running more than one master concurrently. # # If you call this script without any config parameters, it will attempt to run the # init command for all your unicorn configurations listed in /etc/unicorn/*.conf # # /etc/init.d/unicorn start # starts all unicorns # # If you specify a particular config, it will only operate on that one: # # /etc/init.d/unicorn start /etc/unicorn/my_app.conf set -e sig () { test -s "$PID" && kill -$1 `cat "$PID"` } oldsig () { test -s "$OLD_PID" && kill -$1 `cat "$OLD_PID"` } cmd () { case $1 in start) sig 0 && echo >&2 "Already running" && return; echo "Starting"; eval $CMD; ;; stop) sig QUIT && echo "Stopping" && return; echo >&2 "Not running"; ;; force-stop) sig TERM && echo "Forcing a stop" && return; echo >&2 "Not running" ;; restart|reload) sig USR2 && sleep 5 && oldsig QUIT && echo "Killing old master" `cat $OLD_PID` && return; echo >&2 "Couldn't reload, starting '$CMD' instead" eval $CMD ;; upgrade) sig USR2 && echo Upgraded && return; echo >&2 "Couldn't upgrade, starting '$CMD' instead" eval $CMD ;; rotate) sig USR1 && echo rotated logs OK && return; echo >&2 "Couldn't rotate logs" && exit 1 ;; *) echo >&2 "Usage: $0 <start|stop|restart|upgrade|rotate|force-stop>" exit 1 ;; esac } setup () { echo -n "$RAILS_ROOT: " cd $RAILS_ROOT || exit 1 export PID=$UNICORN_PID export OLD_PID="$PID.oldbin" # We need to source .bash_profile to enable RVM! UNICORN_CMD="source ~/.bash_profile; unicorn_rails -c config/unicorn.rb -E $RAILS_ENV -D" CMD="su -s /bin/bash stanislaw -c \"$UNICORN_CMD\"" } start_stop () { # either run the start/stop/reload/etc command for every config under /etc/unicorn or just do it for a specific one # $1 contains the start/stop/etc command # $2 if it exists, should be the specific config we want to act on if [ $2 ]; then . $2 setup cmd $1 else for CONFIG in /etc/unicorn/*.conf; do # import the variables . $CONFIG setup # run the start/stop/etc command cmd $1 done fi } ARGS="$1 $2" start_stop $ARGS
Этот скрипт предполагает, что для каждого приложения в папке /etc/unicorn/ создаётся отдельный файл, например:
/etc/unicorn/first_app.conf
#!/bin/sh RAILS_ENV=production UNICORN_PID=/home/stanislaw/apps/first_app/shared/pids/unicorn.pid RAILS_ROOT=/home/stanislaw/apps/first_app/current
Чтобы при перезагрузке VPS сервера все Unicorn’ы запускались, нужно обязательно изменить порядок запуска startup-скриптов в /etc/rc3.d (сервер работает на runlevel 3): unicorn должен идти после mysql, т.е. файл SЦИФРАunicorn в директории /etc/rc3.d/ должен иметь цифру большую, чем файл SЦИФРАmysqld
Разные версии Ruby
По умолчанию каждое приложение из /home/stanislaw/apps/ будет использовать версию Ruby 1.9.3, которая была установлена в начале. Предположим, что для приложения second_app нам нужна версия 1.8.7. Устанавливаем её дополнительно:
rvm install 1.8.7
и создаём в корне приложения second_app файл .rvmrc:
# http://stackoverflow.com/a/5143967/598057 rvm use 1.8.7
MySQL
В целях безопасности рекомендуется отключить сетевой порт 3306, на который по умолчанию настроен MySQL. В файл/etc/my.cnf добавляем:
skip-networking
И в обоих приложениях исправляем в config/database.yml секцию production:
production: adapter: mysql2 database: albumer_production username: stanislaw password: stanislaw-cool-pass encoding: utf8 socket: /var/lib/mysql/mysql.sock
Разное
Если при работе с сервером наблюдаются такие строки:
tput: unknown terminal "rxvt-unicode"
подойдёт такое решение:
Solution 1: The terminfo file /usr/share/terminfo/r/rxvt-unicode is missing from the remote box. scp it from your laptop.
Обратная связь
За написание этой статьи мы выносим благодарность Станиславу, который подробно описал и оформил данную статью для нашего блога. Вы также имеете возможность опубликовать свой материал у нас на сайте. А за интересные статьи мы будем предоставлять нашим пользователям подарки
Возможно, вас заинтересует
Вероятно, многие слышали о Cloudflare – сети доставки контента и защиты от DDoS. Более 100...
Друзья, мы хотим посвятить этот пост нашему Облаку. Дело в том, что мы давно...
Получи многофункциональную ледянку в подарок к любому тарифу! Зима – это пора мандарин и...