Skip to content

Instantly share code, notes, and snippets.

@eaconde
Last active February 8, 2018 07:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eaconde/ecbc63d4ab4699fdd7fd to your computer and use it in GitHub Desktop.
Save eaconde/ecbc63d4ab4699fdd7fd to your computer and use it in GitHub Desktop.

Project Configuration

Create the rails project

$ rails new capistrano-rails
$ cd capistrano-rails

Initialize Git

$ git init
$ git add .
$ git commit -am 'initial commit'
$ git remote add git@github.com:{username}/capistrano-rails.git
$ git push origin master
..

Add to .gitignore

.DS_Store

/config/database.yml
/config/secrets.yml
/config/local_env.yml
/public/assets

Edit Gemfile

$ vi Gemfile

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

Bundle!

$ bundle install

Add unicorn server config

$ vim config/unicorn/production.rb

# set path to application
app_dir = File.expand_path("../../..", __FILE__)
shared_dir = "/home/deployer/apps/capistrano-rails/shared"
working_directory app_dir


# Set unicorn options
worker_processes 2
preload_app true
timeout 30

# Set up socket location
listen "#{shared_dir}/tmp/sockets/unicorn.sock", :backlog => 64

# Logging
stderr_path "#{shared_dir}/log/unicorn.stderr.log"
stdout_path "#{shared_dir}/log/unicorn.stdout.log"

# Set master PID location
pid "#{shared_dir}/tmp/pids/unicorn.pid"

Install Capistrano

$ bundle exec cap install

Modify Capfile to include required assets

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

# Include default deployment tasks
require 'capistrano/deploy'

# Include tasks from other gems included in your Gemfile
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.rb config

# config valid only for current version of Capistrano
lock '3.4.0'

set :application, 'capistrano-rails'
set :repo_url, 'git@github.com:eaconde/capistrano-rails.git'

# 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/secrets.yml')

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

namespace :deploy do
  desc "Restart unicorn server"
  task :restart do
    invoke 'unicorn:stop'
    invoke 'unicorn:start'
  end
  after 'deploy:publishing', 'deploy:restart'
  
  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end

end

Update deploy/production.rb config file

$ vim config/deploy/production.rb

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

server '[Update me after we know the droplet's IP]',
  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    

Create environment variables file

Generate keys with "rake secret"
$ vim ~/apps/capistrano-rails/shared/config/local_env.yml

SECRET_TOKEN: 1234567890...
SECRET_KEY_BASE: 1234567890...

Modify current application.rb to load and read local_env.yml

# config/application.rb

require File.expand_path('../boot', __FILE__)

require 'rails/all'
require 'yaml' 

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module CapistranoRails
  class Application < Rails::Application
    # Do not swallow errors in after_commit/after_rollback callbacks.
    config.active_record.raise_in_transactional_callbacks = true

    config.before_configuration do
      env_file = File.join(Rails.root, 'config', 'local_env.yml')
        YAML.load(File.open(env_file)).each do |key, value|
        ENV[key.to_s] = value
      end if File.exists?(env_file)
    end
  end
end

Create default page

$ rails g controller welcome index

Set default route

# config/routes.rb

Rails.application.routes.draw do
  root to: "welcome#index"
end

Server Connection Configuration

We now go to Digital Ocean and create our droplet as a root user. Once we have the IP of the droplet, edit the deploy/production.rb file once more

$ vim config/deploy/production.rb
...
server '123.123.123.123',
...

SSH into the droplet as root

$ ssh root@123.123.123.123
.. change password ..

Create our deployer user

$ add user deployer
.. set initial password ..
$ visudo

Update priveleges

deployer ALL=(ALL:ALL) ALL

NOTE: Other than having a single user, some can opt to add a deployers group instead and then add users under that group as stated here

Modify sshd_config file to allow deployer

$ vi /etc/ssh/sshd_config

Port 1025..65536 # Change
UseDNS no # Add
AllowUsers deployer # Add

Open a new shell window and login as deployer to verify that we have connection

$ ssh deployer@123.123.123.123

Create the ssh directory then exit

$ mkdir ~.ssh
$ exit

We will now add our public SSH key to the server by doing a cat command

$ cat ~.ssh/id_rsa.pub | ssh deployer@123.123.123.123 'cat >> ~.ssh/authorized_keys'

Login again to the server. This time, it should no longer ask for the deployer's password

$ ssh deployer@123.123.123.123

While logged in, we can now update the sshd_config file to make our server more secure. Note here we will use sudo
$ sudo vi /etc/ssh/sshd_config

PermitRootLogin no # Change
PasswordAuthentication no # Uncomment

Finally, setup deployer's SSH key

$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

Load the key to the ssh-agent
$ eval "$(ssh-agent -s)"
$ ssh-add ~/.ssh/id_rsa

Configure server connection to Git

$ cat ~/.ssh/id_rsa.pub

Copy the text and add it to your accounts [valid SSH keys](https://github.com/settings/ssh)

$ ssh -T git@github.com
.. should display successful message ..

Configure Server Environment

Update system files

$ sudo apt-get update

Install CURL

$ sudo apt-get install curl

Install RVM

Download GPG Keys
$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
$ curl -L get.rvm.io | bash -s stable
$ source ~/.rvm/scripts/rvm
$ rvm requirements

Ruby 2.2.3 is the stable version from the date of this writing
$ rvm install 2.2.3 
$ rvm use 2.2.3 --default
$ rvm rubygems current

Install Git

$ sudo apt-get install git-core

Install Bundler

$ gem install bundler

Install JavaScript runtime.

$ sudo apt-get install nodejs

Error encountered installing json 1.8.3 on ruby 2.2.x. Solution is to install missing libgmp

$ sudo apt-get install libgmp3-dev

Install MySQL

$ sudo apt-get install mysql-server-5.6
.. set root password ..

Adding Swap Memory

If you encountered an error like I did saying something like

invoke-rc.d: initscript mysql, action "start" failed.
dpkg: error processing package mysql-server-5.6 (--configure):

It is probably just an issue with the memory. To resolve this, simply add a swapfile on the server like so

$ sudo swapon -s
$ sudo dd if=/dev/zero of=/swapfile bs=1024 count=1024k
$ sudo mkswap /swapfile
$ sudo swapon /swapfile
$ sudo vim /etc/fstab

Add the following line to the fstab to make the swap permanent.
    /swapfile       none    swap    sw      0       0 
    
You can then reinstall MySQL 
$ sudo apt-get install mysql-server-5.6

Should display a message like so:
mysql start/running, process 21502

Add MySQL user for deployer and add blank database

$ mysql -u root -p

For localhost access only
mysql> CREATE USER 'deployer'@'localhost' IDENTIFIED BY 'your_password';
mysql> GRANT ALL PRIVILEGES ON *.* TO 'deployer'@'localhost' WITH GRANT OPTION;

For connections from any host
mysql> CREATE USER 'deployer'@'%' IDENTIFIED BY 'your_password';
mysql> GRANT ALL PRIVILEGES ON *.* TO 'deployer'@'%' WITH GRANT OPTION;

Create our database
mysql> CREATE DATABASE capistrano-rails

Edit mysql my.cnf

$ sudo vi /etc/mysql/my.cnf

Edit entry
bind-address = [Droplet IP]

Prevent error installing mysql2 gem

$ sudo apt-get install libmysqlclient-dev

Create the required symlink files database.yml, secrets.yml, and local_env.yml IMPORTANT: Never commit actual database logins in git

database.yml

$ vim ~apps/capistrano-rails/shared/config/database.yml

default: &mysql
  adapter: mysql2
  username: 'deployer'
  password: 'deployer_password'
  host: 'localhost'

development:
  <<: *mysql
  database: capistrano_rails_dev

test:
  <<: *mysql
  database: capistrano_rails_test

production:
  <<: *mysql
  database: capistrano_rails

secrets.yml

Generate keys with "rake secret"
$ vim ~apps/capistrano-rails/shared/config/secrets.yml

development:
  secret_key_base: 1234567890...

test:
  secret_key_base: 1234567890...

production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

local_env.yml

Generate keys with "rake secret"
$ vim ~/apps/capistrano-rails/shared/config/local_env.yml

SECRET_TOKEN: 1234567890...
SECRET_KEY_BASE: 1234567890...

Setup Unicorn init file

$ sudo vi /etc/init.d/unicorn_capistrano-rails

#!/bin/sh

### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the unicorn app server
# Description:       starts unicorn using start-stop-daemon
### END INIT INFO

set -e

USAGE="Usage: $0 <start|stop|restart|upgrade|rotate|force-stop>"

# app settings
USER="deployer"
APP_NAME="capistrano-rails"
APP_ROOT="/home/$USER/apps/$APP_NAME"
ENV="production"

# environment settings
PATH="/home/$USER/.rbenv/shims:/home/$USER/.rbenv/bin:$PATH"
CMD="cd $APP_ROOT/current && bundle exec unicorn -c config/unicorn/production.rb -E $ENV -D"
PID="$APP_ROOT/shared/tmp/pids/unicorn.pid"
OLD_PID="$PID.oldbin"

# make sure the app exists
cd $APP_ROOT || exit 1

sig () {
  test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
  test -s $OLD_PID && kill -$1 `cat $OLD_PID`
}

case $1 in
start)
    sig 0 && echo >&2 "Already running" && exit 0
    echo "Starting $APP_NAME"
    su - $USER -c "$CMD"
    ;;
  stop)
    echo "Stopping $APP_NAME"
    sig QUIT && exit 0
    echo >&2 "Not running"
    ;;
  force-stop)
    echo "Force stopping $APP_NAME"
    sig TERM && exit 0
    echo >&2 "Not running"
    ;;
  restart|reload|upgrade)
    sig USR2 && echo "reloaded $APP_NAME" && exit 0
    echo >&2 "Couldn't reload, starting '$CMD' instead"
    $CMD
    ;;
  rotate)
    sig USR1 && echo rotated logs OK && exit 0
    echo >&2 "Couldn't rotate logs" && exit 1
    ;;
  *)
    echo >&2 $USAGE
    exit 1
    ;;
esac

Update the script's permissions and enable Unicorn to start on boot

$ sudo chmod 755 /etc/init.d/unicorn_capistrano-rails
$ sudo update-rc.d unicorn_capistrano-rails defaults
$ sudo service unicorn_capistrano-rails start

Install and Configure Nginx

$ sudo apt-get install nginx

Once installed check if it is running
$ sudo service nginx status
 * nginx is running

Edit site config file

$ sudo vim /etc/nginx/sites-enabled/default

upstream unicorn {
  server unix:/home/deployer/apps/capistrano-rails/shared/tmp/sockets/unicorn.sock fail_timeout=0;
}

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

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

  location ~ ^/(robots.txt|sitemap.xml.gz)/ {
    root /home/deployer/apps/capistrano-rails/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;
}

Restart Nginx server

$ sudo service nginx reload
 * Reloading nginx configuration nginx
$ sudo service nginx status
 * Restarting nginx nginx

Running deploy

$ cap production deploy

Error mv: cannot overwrite directory ‘/home/deployer/apps/capistrano-rails/current’ with non-directory

Make sure that the directory /home/deployer/apps/capistrano-rails/current is empty.
$ cd ~/apps/capistrano-rails
$ rm -rf current

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment