1. Создаем инстанс Amazon AWS EC2

В этом примере, я буду использовать Ubuntu Server 16.04 LTS.

Запускаем создание инстанса, кнопка Launch Instance

Выбираем ОС и различные параметры.

На 6 шаге в Configure Security Group можно сразу задать правила для доступа, например открываем наружу 80 или 443 порт, а доступ по SSH на 22 порт разрешаем только себе, заодно для себя можно открыть доступ к порту 2812, его использует веб-интерфейс сервиса monit который мы будем использовать для управления сервисами.

Жмем кнопку Review and Launch проверяем конфигурацию и жмем Launch появится вопрос о создании или использовании существующей пары ключей, если ключа нет, создаем новый и скачиваем его.

Завершаем создание нажатием на кнопку Launch Instance

Для управления инстансами из своей консоли можно установить утилиту AWS CLI

Создаем БД RDS если требуется и настраиваем доступ через Security Group - Inbound Rules.

Для подключения к инстансу по SSH используем сохраненный ключ:

ssh -i /path/my-key-pair.pem ec2-user@ec2-198-51-100-1.compute-1.amazonaws.com

По умолчанию мы работаем под пользователем ubuntu, его и будем использовать для деплоя.

2. Настраиваем сервер

Настраиваем сервер с помощью скрипта или вручную:

#!/bin/bash

# INSTALLATION SCRIPT

uname -srm
cat /etc/*-release

USER_NAME=ubuntu
HOME_PATH="/home/$USER_NAME"

# VERSIONS

DEPLOYER_RVM_RUBY_VERSION=ruby-2.4.0
DEPLOYER_RVM_GEMSET_NAME=qna

# Задаем пароль пользователю ubuntu
# DEFINE PASSWORDS

DEPLOYER_PASSWORD=`openssl rand -base64 24`

echo "$USER_NAME:$DEPLOYER_PASSWORD" | chpasswd

# SET LANG VARS

echo 'export LC_ALL="en_US.UTF-8"'   >> ~/.bashrc
echo 'export LANGUAGE="en_US:en"'    >> ~/.bashrc
echo 'export LANG="en_US.UTF-8"'     >> ~/.bashrc
echo 'export LC_CTYPE="en_US.UTF-8"' >> ~/.bashrc

source ~/.bashrc

# VIM OPTIONS

echo "set nocompatible" > ~/.vimrc
echo ":set backspace=indent,eol,start" >> ~/.vimrc

echo "set nocompatible" >> $HOME_PATH/.vimrc
echo ":set backspace=indent,eol,start" >> $HOME_PATH/.vimrc
chown $USER_NAME:$USER_NAME $HOME_PATH/.vimrc
chmod 0644 $HOME_PATH/.vimrc

# FOR GEM INSTALL

echo 'gem: --no-ri --no-rdoc' >> $HOME_PATH/.gemrc
chown $USER_NAME:$USER_NAME $HOME_PATH/.gemrc
chmod -R 0644 $HOME_PATH/.gemrc

# CREATE SWAP

dd if=/dev/zero of=/swapfile bs=1M count=1024
chmod 0600 /swapfile
mkswap /swapfile
swapon /swapfile

echo "/swapfile none swap sw 0 0" >> /etc/fstab

echo 10 | tee /proc/sys/vm/swappiness
echo vm.swappiness = 10 | tee -a /etc/sysctl.conf
sysctl --system

# MINIMAL SOFT PACK

apt-get clean
apt-get update && apt-get upgrade

apt-get install -y coreutils htop monit unzip wget curl mc vim zsh tmux git-core

sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

# REQUIREMENTS LIBS

apt-get install libmysqlclient-dev libpq-dev

# IMAGE MAGICK

apt-get install -y imagemagick libmagickwand-dev
convert --version

# REDIS

apt-get install -y redis-server
cp /etc/redis/redis.conf /etc/redis/redis.conf.default
systemctl restart redis

# NGINX

apt-get install nginx -y

# SPHINX SEARCH

apt-get install sphinxsearch

# Устанавливаем PostgreSQL если нужно

apt-cache pkgnames postgresql
apt-get install -y postgresql postgresql-contrib libpq-dev

# RVM REQUIREMENTS

gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
\curl -sSL https://get.rvm.io | bash -s stable
/usr/local/rvm/bin/rvm requirements

(echo 'yes') | (/usr/local/rvm/bin/rvm implode)


echo "--------------------------------------------------" >> $HOME_PATH/credentials.txt
echo "DEPLOYER LOGIN/PASSWORD"                            >> $HOME_PATH/credentials.txt
echo "--------------------------------------------------" >> $HOME_PATH/credentials.txt

echo "
LOGIN: $USER_NAME
PASSWORD: $DEPLOYER_PASSWORD
"  >> $HOME_PATH/credentials.txt

cat $HOME_PATH/credentials.txt

3. Настраиваем Capistrano

В Gemfile пишем:

group :development do
  # Use Capistrano for deployment
  gem 'capistrano', require: false
  gem 'capistrano-bundler', require: false
  gem 'capistrano-rails', require: false
  gem 'capistrano-rvm', require: false
  gem 'capistrano3-puma',   require: false
  gem 'capistrano-sidekiq', require: false
end

Запускаем команду cap install, это создаст файлы:

config/deploy.rb config/deploy/staging.rb config/deploy/production.rb Capfile И директорию lib/capistrano/tasks

В Capfile добавляем нужные инструменты:

# Load DSL and set up stages
require 'capistrano/setup'

# Include default deployment tasks
require 'capistrano/setup'
require 'capistrano/deploy'

require 'capistrano/rails'
require 'capistrano/bundler'
require 'capistrano/rvm'
require 'capistrano/puma'
require 'capistrano/puma/nginx'

require 'capistrano/sidekiq'
require 'capistrano/sidekiq/monit' #to require monit tasks # Only for capistrano3

require 'whenever/capistrano'
require 'thinking_sphinx/capistrano'

install_plugin Capistrano::Puma  # Default puma tasks
# install_plugin Capistrano::Puma::Workers  # if you want to control the workers (in cluster mode)
# install_plugin Capistrano::Puma::Jungle # if you need the jungle tasks
install_plugin Capistrano::Puma::Monit  # if you need the monit tasks
install_plugin Capistrano::Puma::Nginx  # if you want to upload a nginx site template

Список доступных задач можно посмотреть в консоли cap -T

Настраиваем конфигурацию в config/deploy.rb:

# config valid only for current version of Capistrano
lock "3.8.0"

set :repo_url,        'git@github.com:EugeneKey/QnA.git'
set :application,     'qna'
set :user,            'ubuntu'
set :puma_threads,    [4, 16]
set :puma_workers,    0

# Default branch is :master
# ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp

# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, [])
        .push('config/database.yml', 'config/private_pub.yml', 
              'config/private_pub_puma.rb', '.env')

# Default value for linked_dirs is []
set :linked_dirs, fetch(:linked_dirs, [])
        .push('bin', 'log', 'tmp/pids', 'tmp/cache', 
              'tmp/sockets', 'vendor/bundle', 'public/system', 
              'public/uploads')

namespace :puma do
  desc 'Create Directories for Puma Pids and Socket'
  task :make_dirs do
    on roles(:app) do
      execute "mkdir #{shared_path}/tmp/sockets -p"
      execute "mkdir #{shared_path}/tmp/pids -p"
    end
  end

  before :start, :make_dirs
end

namespace :private_pub do
  desc 'Start private_pub server'
  task :start do
    on roles(:app) do
      within current_path do
        with rails_env: fetch(:rails_env) do
          execute :bundle, "exec pumactl -F config/private_pub_puma.rb start"
        end
      end
    end
  end

  desc 'Stop private_pub server'
  task :stop do
    on roles(:app) do
      within current_path do
        with rails_env: fetch(:rails_env) do
          execute :bundle, "exec pumactl -F config/private_pub_puma.rb stop"
        end
      end
    end
  end

  desc 'Restart private_pub server'
  task :restart do
    on roles(:app) do
      within current_path do
        with rails_env: fetch(:rails_env) do
          execute :bundle, "exec pumactl -F config/private_pub_puma.rb restart"
        end
      end
    end
  end
end

namespace :deploy do
  desc "Make sure local git is in sync with remote."
  task :check_revision do
    on roles(:app) 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
  end

  desc 'Initial Deploy'
  task :initial do
    on roles(:app) do
      before 'deploy:restart', 'puma:start', 'private_pub:start'
      invoke 'deploy'
    end
  end

  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      invoke 'puma:restart'
      invoke 'private_pub:restart'
    end
  end

  before :starting,     :check_revision
  after  :finishing,    :compile_assets
  after  :finishing,    :cleanup
  after  :finishing,    :restart

end

В файле config/deploy/production.rb указываем сервер, путь к rsa ключу и остальные параметры:

server '123.123.123.123', port: 22, roles: [:web, :app, :db], primary: true
set :ssh_options,     { forward_agent: true, user: fetch(:user), 
                        keys: %w(~/.ssh/amazon_rsa_key.pem), 
                        auth_methods: %w(publickey password) }
# server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value
# server 'db.example.com', user: 'deploy', roles: %w{db}

set :rails_env, :production

set :pty,             false
set :use_sudo,        false
set :stage,           :production
set :deploy_via,      :remote_cache
set :deploy_to,       "/home/#{fetch(:user)}/#{fetch(:application)}"
set :puma_bind,       "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
set :puma_state,      "#{shared_path}/tmp/pids/puma.state"
set :puma_pid,        "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{release_path}/log/puma.error.log"
set :puma_error_log,  "#{release_path}/log/puma.access.log"
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true  # Change to false when not using ActiveRecord

set :nginx_server_name, 'myrailsproject.com'

set :sidekiq_options_per_process, ["--queue default --queue mailers"]

После настройки параметров можно попробовать задеплоить приложение.

Для этого, комитим изменения и отправляем на github:

git add .
git commit -am "Name commit"
git push

И выполняем

cap production deploy:initial

Если появляются ошибки исправляем, комитим изменения и пробуем заново.

После первого комита нужно будет создать некоторые файлы вручную, это общие файлы конфигурации указаные в :linked_files:

config/database.yml config/private_pub.yml config/private_pub_puma.rb .env

Файлы нужно поместить в папку shared в папке деплоя путь к которой задан в конфиге set :deploy_to

Есть возможность настроить конфиг Puma и Nginx, для этого нужно выгрузить конфиги:

rails g capistrano:nginx_puma:config

А потом залить их на сервер: cap production puma:config cap production puma:nginx_config

и отредактировать их.

Настраиваем конфигурацию monit

После успешного деплоя, для контроля и управления сервисами можно использовать monit.

Создаем конфиг monit, добавляем нужные сервисы:

### Unicorn ###
check process unicorn
  with pidfile "/home/deployer/qna/current/tmp/pids/unicorn.pid"
  start program = "/bin/su - deployer -c 'cd /home/deployer/qna/current && ( RAILS_ENV=production ~/.rvm/bin/rvm default do bundle exec unicorn -c /home/deployer/qna/current/config/unicorn/production.rb -E deployment -D  )'"
  stop program = "/bin/su - deployer -c 'cd /home/deployer/qna/current && /usr/bin/env kill -s QUIT `cat /home/deployer/qna/current/tmp/pids/unicorn.pid`'"
  if memory usage > 90% for 3 cycles then restart
  if cpu > 90% for 2 cycles then restart
  if 5 restarts within 5 cycles then timeout

### Puma ###
check process puma
  with pidfile "/home/ubuntu/qna/shared/tmp/pids/puma.pid"
  start program = "/bin/su - ubuntu -c 'cd /home/ubuntu/qna/current && ~/.rvm/bin/rvm default do bundle exec puma -C /home/ubuntu/qna/shared/puma.rb --daemon'"
  stop program = "/bin/su - ubuntu -c 'cd /home/ubuntu/qna/current && ~/.rvm/bin/rvm default do bundle exec pumactl -S /home/ubuntu/qna/shared/tmp/pids/puma.state stop'"
  if cpu > 80% then restart
  if memory usage > 80% for 2 cycles then restart
  if 3 restarts within 3 cycles then timeout

### Nginx Passenger ###
check process nginx
  with pidfile /opt/nginx/logs/nginx.pid
  start program = "/etc/init.d/nginx start"
  stop program = "/etc/init.d/nginx stop"
  if cpu > 60% for 2 cycles then alert
  if cpu > 80% for 5 cycles then restart
  if memory usage > 80% for 5 cycles then restart
  if failed host 127.0.0.1 port 80 protocol http then restart
  if 3 restarts within 5 cycles then timeout

### Nginx ###
check process nginx with pidfile /var/run/nginx.pid
  group www
  group nginx
  start program = "/etc/init.d/nginx start"
  stop program = "/etc/init.d/nginx stop"
  if cpu > 60% for 2 cycles then alert
  if cpu > 80% for 5 cycles then restart
  if memory usage > 80% for 5 cycles then restart
  if failed port 80 protocol http request "/" then restart
  if 5 restarts with 5 cycles then timeout
  depend nginx_bin
  depend nginx_rc

check file nginx_bin with path /usr/sbin/nginx
  group nginx
  include /etc/monit/templates/rootbin

check file nginx_rc with path /etc/init.d/nginx
  group nginx
  include /etc/monit/templates/rootbin

### Postgresql ###
check process postgresql
  with pidfile "/var/run/postgresql/9.3-main.pid"
  start program = "/usr/sbin/service postgresql start"
  stop  program = "/usr/sbin/service postgresql stop"
  if failed host localhost port 5432 protocol pgsql then restart
  if cpu > 80% then restart
  if memory usage > 80% for 2 cycles then restart
  if 5 restarts within 5 cycles then timeout

### Redis ###
check process redis-server
    with pidfile "/var/run/redis/redis-server.pid"
    start program = "/etc/init.d/redis-server start"
    stop program = "/etc/init.d/redis-server stop"
    if totalmem > 100 Mb then alert
    if children > 255 for 5 cycles then stop
    if cpu usage > 95% for 3 cycles then restart
    if memory usage > 80% for 5 cycles then restart
    if failed host 127.0.0.1 port 6379 then restart
    if 5 restarts within 5 cycles then timeout

### Sidekiq ###
check process sidekiq
  with pidfile "/home/ubuntu/qna/shared/tmp/pids/sidekiq-0.pid"
  start program = "/bin/su - ubuntu -c 'cd /home/ubuntu/qna/current && ~/.rvm/bin/rvm default do bundle exec sidekiq  --index 0 --pidfile /home/ubuntu/qna/shared/tmp/pids/sidekiq-0.pid --environment production  --logfile /home/ubuntu/qna/shared/log/sidekiq.log   --queue default --queue mailers -d'" with timeout 30 seconds
  stop program = "/bin/su - ubuntu -c 'cd /home/ubuntu/qna/current && ~/.rvm/bin/rvm default do bundle exec sidekiqctl stop /home/ubuntu/qna/shared/tmp/pids/sidekiq-0.pid'" with timeout 20 seconds
  if cpu > 80% then restart
  if memory usage > 80% for 2 cycles then restart
  if 3 restarts within 3 cycles then timeout

### Sphinx ###
check process sphinx
  with pidfile "/home/deployer/qna/shared/log/production.sphinx.pid"
  start program = "/bin/su - deployer -c 'cd /home/deployer/qna/current && /home/deployer/.rvm/bin/rvm default do bundle exec rake RAILS_ENV=production ts:start'"
  stop program = "/bin/su - deployer -c 'cd /home/deployer/qna/current && /home/deployer/.rvm/bin/rvm default do bundle exec rake RAILS_ENV=production ts:stop'"
  if cpu > 80% then restart
  if memory usage > 80% for 2 cycles then restart
  if 3 restarts within 3 cycles then timeout

### Thin (private_pub) ###
check process thin
  with pidfile "/home/deployer/qna/shared/tmp/pids/thin.pid"
  start program = "/bin/su - deployer -c 'cd /home/deployer/qna/current && RAILS_ENV=production /home/deployer/.rvm/bin/rvm default do bundle exec thin -C config/private_pub_thin.yml start'"
  stop program = "/bin/su - deployer -c 'cd /home/deployer/qna/current && RAILS_ENV=production /home/deployer/.rvm/bin/rvm default do bundle exec thin -C config/private_pub_thin.yml stop'"
  if cpu > 80% then restart
  if memory usage > 80% for 2 cycles then restart
  if 3 restarts within 3 cycles then timeout

### Puma (private_pub) ###
check process private_pub
  with pidfile "/home/ubuntu/qna/shared/tmp/pids/puma_private_pub.pid"
  start program = "/bin/su - ubuntu -c 'cd /home/ubuntu/qna/current && RAILS_ENV=production ~/.rvm/bin/rvm default do bundle exec pumactl -F /home/ubuntu/qna/shared/config/private_pub_puma.rb start'"
  stop program = "/bin/su - ubuntu -c 'cd /home/ubuntu/qna/current && RAILS_ENV=production ~/.rvm/bin/rvm default do bundle exec pumactl -F /home/ubuntu/qna/shared/config/private_pub_puma.rb stop'"
  if cpu > 80% then restart
  if memory usage > 80% for 2 cycles then restart
  if 3 restarts within 3 cycles then timeout

перезапускаем monit:

sudo monit reload

по умолчанию веб-интерфейс отключен, включаем в конфиге /etc/monit/monitrc, раскомментировав нужные строки:

set httpd port 2812 and                                                     
#     use address localhost  # only accept connection from localhost        
#     allow localhost        # allow localhost to connect to the server and 
    allow 10.0.2.2                                                          
    allow admin:monit      # require user 'admin' with password 'monit'

Особенности деплоя на другие VPS серверы

У Digital Ocean и других VPS, процесс деплоя ничем не отличается, но скорее всего потребуется создать пользователя для деплоя, так как изначально дается рутовый доступ по SSH, а это не очень хорошо.

После создания пользователя нужно создать RSA ключ и настроить доступ по SSH для этого пользователя с использованием ключа и отключить доступ для root пользователя.

Есть еще один интересный вариант деполоя, это Docker, который используется и при разработке и при деплое, об этом напишу в другой раз.