Automating Deployment in Rails

As you would’ve guessed, this post involves us having to configure our deployment via Capistrano.

Yesterday, I did a gist of the whole process it took me to have it completed. The whole solution assumes the use of the following resources:

  • Rails 4.2.1
  • Capistrano 3.4
  • MySQL 5.6
  • Unicorn 4.9
  • Nginx
  • Ubuntu 14.04 x64 (Hosted on Digital Ocean)

First off, I start with creating a new RoR project setting up the standard git remote, gems and bundle install. After which, I then configured unicorn’s production config which will be called when we run deployment.

I had several iterations before getting the current content. The exception I experienced is related to the unicorn server not being able to properly start/stop proof of which is that the pids never gets deleted causing assets to not get refreshed on the browser. It is important to make sure that the path the file being accessed by unicorn is the correct path.

I then move on to configuring my Capistrano gem. Installed the default files via the command

$ bundle exec cap install

Configuring the Cap files are pretty straightfoward. In the gist, we specify the name of the project, the git repo, the linked files & folders, then finally our cap tasks. Linked files needs to be created in the server manually under the /shared directory. In our sample, we created 3 files that is the database.yml, secrets.yml, and local_env.yml. These files are then referenced by the current and releases directories of the project. Cap tasks on the other hand are just like rake tasks which we can execute on certain points of the process as specified in the Capistrano’s flow documentation. We added our own unicorn:stop and unicorn:start (or unicorn:reload/unicorn:restart) after deploy. Finally, we created a simple controller with an index action and set it as the root route for the project.

Second step is configuring the server. We add a new user to connect via SSH only, we remove the root, and allow multiple ports for SSH connections. After that is done, we then configure our public key to be able to download from git.

Step three is finally configuring the server to include our project’s environment requirements. We install CURL, RVM, Git, Bundler, and Javascript Runtime. We then install and configure our database which is MySQL. A notable experience I had after I installed MySQL was being unable to start the service. Since I was only running on 512MB of memory, the server was unable to process my request. What I did was to allot a swap space so that the request can be accommodated even when the memory is full. I then add a new deployer user to mysql and create the new database for the project.

Step four is simply creating the linked files as stated above. Simply copy the current content from the RoR project and replace key values (secret keys, and database login) on the server. After that, we will then configure our unicorn shell script (init.d). This file is pretty standard across the net and what will differ is basically the path you used for the project. Ultimately, the goal of this file is to be able to start/stop our unicorn server by executing the command

$ cd /home/deployer/apps/capistrano-rails/current && ( RAILS_ENV=production ~/.rvm/bin/rvm default do bundle exec unicorn -c /home/deployer/apps/capistrano-rails/current/config/unicorn/production.rb -E deployment -D  )

When started, it should create a new .pid under /shared/tmp/pids and a .sock under /shared/tmp/sockets (this will be referenced by Nginx later). And delete those files on stop. Once installed , Nginx configuration is also straightforward. As shown on the gist, we simply provided an upstream block pointing to our unicorn sock file (created when we deployed). That is then looked up on Nginx proxy which in turn renders our files.

Once everything is in place, every time we need to update the server, we can just simply push our changes using the command.

 $ cap production deploy 

Additionally, you may opt to add several things to this setup such as executing the test suite first before deploying. Or creating the project it’s own file in the site-enabled. There is room for improvement with this setup. Nevertheless, this configuration will get your deployment automation up and running in no time at all.

 

References:

http://capistranorb.com/documentation/getting-started/flow/#

DigitalOcean Ubuntu 14.04 x64 + Rails 4 + Nginx + Unicorn + PostgreSQL + Capistrano 3

SSH into Root

$ ssh root@123.123.123.123

Change Root Password

$ passwd

Add Deploy User

$ adduser deployer

Update Sudo Privileges

$ visudo
username ALL=(ALL:ALL) ALL

Configure SSH

$ vi /etc/ssh/sshd_config
Port 22 # Change (1025..65536)
Protocol 2 # Change
PermitRootLogin no  # Change
UseDNS no # Add
AllowUsers deployer # Add

Reload SSH

$ reload ssh

SSH with Deploy User (Don’t close root)

$ ssh -p 1026 deployer@123.123.123.123

Install Curl

$ sudo apt-get update
$ sudo apt-get install curl

Install RVM

$ curl -L get.rvm.io | bash -s stable
$ source ~/.rvm/scripts/rvm
$ rvm requirements
$ rvm install 2.2.0
$ rvm use 2.2.0 --default
$ rvm rubygems current

Install PostgreSQL

$ sudo apt-get install postgresql postgresql-server-dev-9.3
$ gem install pg -- --with-pg-config=/usr/bin/pg_config

Create Postgres User

$ sudo -u postgres psql
create user deployer with password 'password';
alter role deployer superuser createrole createdb replication;
create database MYAPP_production owner deployer;

Install GIT

$ sudo apt-get install git-core

Install Bundler

$ gem install bundler

Setup Nginx

$ sudo apt-get install nginx
$ nginx -h
$ cat /etc/init.d/nginx
$ /etc/init.d/nginx -h
$ sudo service nginx start
$ sudo vim /etc/nginx/sites-enabled/default

upstream unicorn {
  server unix:/tmp/unicorn.MYAPP.sock fail_timeout=0;
}

server {
  listen 80 default deferred;
  # server_name example.com;
  root /home/deployer/apps/MYAPP/current/public;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  location ~ ^/(robots.txt|sitemap.xml.gz)/ {
    root /home/deployer/apps/MYAPP/current/public;
  }

  try_files $uri/index.html $uri @unicorn;
  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://unicorn;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 4G;
  keepalive_timeout 10;
}

Add Unicorn

$ vim Gemfile
gem 'unicorn'    

Update Unicorn Config

$ vim config/unicorn/production.rb

root = "/home/deployer/apps/MYAPP/current"
working_directory root

pid "#{root}/tmp/pids/unicorn.pid"

stderr_path "#{root}/log/unicorn.log"
stdout_path "#{root}/log/unicorn.log"

worker_processes Integer(ENV['WEB_CONCURRENCY'])
timeout 30
preload_app true

listen '/tmp/unicorn.spui.sock', backlog: 64

before_fork do |server, worker|
  Signal.trap 'TERM' do
    puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
    Process.kill 'QUIT', Process.pid
  end

  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.connection.disconnect!
end

after_fork do |server, worker|
  Signal.trap 'TERM' do
    puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT'
  end

  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.establish_connection
end

# Force the bundler gemfile environment variable to
# reference the capistrano "current" symlink
before_exec do |_|
  ENV['BUNDLE_GEMFILE'] = File.join(root, 'Gemfile')
end

Add Capistrano

$ vim Gemfile

group :development do
  gem 'capistrano-rails'
  gem 'capistrano-rvm'
  gem 'capistrano3-unicorn'
end

Install Capistrano

$ bundle exec cap install

Update Capistrano Capfile

$ vim Capfile

require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/rvm'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano3/unicorn'

# Load custom tasks from `lib/capistrano/tasks' if you have any defined
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

Update Capistrano Deploy Config

$ vim config/deploy.rb

lock '3.3.5'

set :application, 'spui'
set :repo_url, 'git@github.com:MYGITHUB/MYAPP.git'

ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call

set :use_sudo, false
set :bundle_binstubs, nil
set :linked_files, fetch(:linked_files, []).push('config/database.yml')
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')

after 'deploy:publishing', 'deploy:restart'

namespace :deploy do
  task :restart do
    invoke 'unicorn:reload'
  end
end

Update Production Deploy Config

$ vim config/deploy/production.rb

set :port, 22
set :user, 'deployer'
set :deploy_via, :remote_cache
set :use_sudo, false

server '123.333.333.333',
  roles: [:web, :app, :db],
  port: fetch(:port),
  user: fetch(:user),
  primary: true

set :deploy_to, "/home/#{fetch(:user)}/apps/#{fetch(:application)}"

set :ssh_options, {
  forward_agent: true,
  auth_methods: %w(publickey),
  user: 'deployer',
}

set :rails_env, :production
set :conditionally_migrate, true    

Add SSH Key to DigitalOcean

$ cat ~/.ssh/id_rsa.pub | ssh -p 22 username@123.123.123.123 'cat >> ~/.ssh/authorized_keys'

Say Hi to Github

# follow the steps in this guide if receive permission denied(public key)
# https://help.github.com/articles/error-permission-denied-publickey
$ ssh github@github.com    

Check Deployment (Commit and Push)

$ cap production deploy:check

Deploy

$ cap production deploy

Thanks to James Dullaghan for his help.

https://github.com/TalkingQuickly/capistrano-3-rails-template

https://help.github.com/articles/generating-ssh-keys/

https://www.digitalocean.com/community/tutorials/how-to-use-capistrano-to-automate-deployments-getting-started

 

One thought on “Automating Deployment in Rails

Leave a comment