Setting up a Digital Ocean Droplet (VPS) on Ubuntu with Rails, Nginx, Unicorn, Postgres, Redis and Capistrano

Posted by Andrew on November 16, 2013

Create a Droplet on Digital Ocean

I stuck with the defaults:

Size
512MB / 1 CPU
20GB SSD
$5 / Month

Image
Ubuntu 12.04 x32

ssh to root in terminal with your server's ip address

ssh root@123.123.123.123

Add ssh fingerprint and enter the password provided in the email from DO

Change password

passwd

Create new user. I like calling this guy "deployer" so that's what you'll see throughout this post.

adduser deployer

Set new users privileges

visudo

Find user privileges section and add your new user privileges under root. Type cntrl+x then y to save

# User privilege specification
root  ALL=(ALL:ALL) ALL
deployer ALL=(ALL:ALL) ALL

Configure SSH

nano /etc/ssh/sshd_config

Find and change port to one that isn't default(22 is default: choose between 1025 ..65536)

# Port 22 # change this to whatever port you wish to use I'm using 1026 for this example
Port 1026
Protocol 2
PermitRootLogin yes # You can change this to "no" if you don't want the root user to be able to ssh in anymore

Add to bottom of sshd_config file after changing port (cntrl+x then y to save)

UseDNS no
AllowUsers deployer # Only add this line if you have changed PermitRootLogin to no, or you can do AllowUsers deployer root

Reload ssh

reload ssh

Don't close root! Open a new shell and ssh to your VPS (droplet) with deployer (remember the port you set above or you'll be locked out)

ssh -p 1026 deployer@123.123.123.123

Update the packages as deployer on your VPS. This installs curl, some python stuff, git and node.js (node.js is for the Rails asset pipeline)

sudo apt-get update
sudo apt-get -y install curl python-software-properties git-core nodejs

Install Redis (optional)

Prepare to install Redis

sudo apt-get -y install build-essential tcl8.5

Change to your home directory and download Redis with wget (I use it for Sidekiq)

cd ~
wget http://redis.googlecode.com/files/redis-2.4.16.tar.gz

Untar Redis, switch into its directory then run make and make install. Then run the Redis install script.

tar xzf redis-2.4.16.tar.gz
cd redis-2.4.16
make
make test
sudo make install
cd utils
sudo ./install_server.sh

To automatically start Redis when the server boots

sudo update-rc.d redis_6379 defaults

RVM

Install latest stable version of rvm

curl -L get.rvm.io | bash -s stable

Load rvm

source ~/.rvm/scripts/rvm

Install rvm dependencies

rvm requirements

Install ruby 2.0.0

rvm install 2.0.0

Use 2.0.0 as rvm default

rvm use 2.0.0 --default

Install latest version of rubygems if rvm install didn't

rvm rubygems current

Rails

Install the rails gem

gem install rails --no-ri --no-rdoc

Install bundler

gem install bundler

PostgreSQL

Install postgres

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

Create a new postgres user. It's not a bad idea to use the same username here. Remember these so you can add them to your database.yml file later.

sudo -u postgres psql
create user deployer with password 'secret';
alter role deployer superuser createrole createdb replication;
create database projectname_production owner deployer;
\q

Nginx

Setup nginx

sudo apt-get install nginx

These 3 commands test if nginx is installed

nginx -h
cat /etc/init.d/nginx
/etc/init.d/nginx -h

Start Nginx

sudo service nginx start
# cd /etc/nginx

Preparing for Deployment locally with Unicorn, Nginx and Capistrano

Add unicorn to the gemfile

gem "unicorn"

Manually create these 3 files:

  • config/nginx.conf
  • config/unicorn.rb
  • config/unicorn_init.sh

config/nginx.conf (change projectname and username, I'm using deployer, to match your directory structure!)

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

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

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control 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;
}

config/unicorn.rb

root = "/home/deployer/apps/projectname/current"
working_directory root
pid "#{root}/tmp/pids/unicorn.pid"
stderr_path "#{root}/log/unicorn.log"
stdout_path "#{root}/log/unicorn.log"

listen "/tmp/unicorn.projectname.sock"
worker_processes 2
timeout 30

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

config/unicorn_init.sh

#!/bin/sh
### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Manage unicorn server
# Description:       Start, stop, restart unicorn server for a specific application.
### END INIT INFO
set -e

# Feel free to change any of the following variables for your app:
TIMEOUT=${TIMEOUT-60}
APP_ROOT=/home/deployer/apps/projectname/current
PID=$APP_ROOT/tmp/pids/unicorn.pid
CMD="cd $APP_ROOT; bundle exec unicorn -D -c $APP_ROOT/config/unicorn.rb -E production"
AS_USER=deployer
set -u

OLD_PIN="$PID.oldbin"

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

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

run () {
  if [ "$(id -un)" = "$AS_USER" ]; then
    eval $1
  else
    su -c "$1" - $AS_USER
  fi
}

case "$1" in
start)
  sig 0 && echo >&2 "Already running" && exit 0
  run "$CMD"
  ;;
stop)
  sig QUIT && exit 0
  echo >&2 "Not running"
  ;;
force-stop)
  sig TERM && exit 0
  echo >&2 "Not running"
  ;;
restart|reload)
  sig HUP && echo reloaded OK && exit 0
  echo >&2 "Couldn't reload, starting '$CMD' instead"
  run "$CMD"
  ;;
upgrade)
  if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
  then
    n=$TIMEOUT
    while test -s $OLD_PIN && test $n -ge 0
    do
      printf '.' && sleep 1 && n=$(( $n - 1 ))
    done
    echo

    if test $n -lt 0 && test -s $OLD_PIN
    then
      echo >&2 "$OLD_PIN still exists after $TIMEOUT seconds"
      exit 1
    fi
    exit 0
  fi
  echo >&2 "Couldn't upgrade, starting '$CMD' instead"
  run "$CMD"
  ;;
reopen-logs)
  sig USR1
  ;;
*)
  echo >&2 "Usage: $0 <start|stop|restart|upgrade|force-stop|reopen-logs>"
  exit 1
  ;;
esac
Change permissions on unicornt_init.sh
chmod +x config/unicorn_init.sh

Capistrano

Add capistrano and rvm capistrano to gemfile

gem 'capistrano'
gem 'rvm-capistrano'

Create Capfile & config/deploy.rb files by running

capify .

For deploy.rb you should change the server ip address, :user (I'm using deployer), :repository path (gertig), and note that :port is not 22 since we changed it above to 1026

config/deploy.rb

require "bundler/capistrano"
require "rvm/capistrano"
require 'sidekiq/capistrano'

server "123.123.123.123", :web, :app, :db, primary: true

set :application, "projectname"
set :user, "deployer"
set :port, 1026
set :deploy_to, "/home/#{user}/apps/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false

set :scm, "git"
set :repository, "git@github.com:gertig/reponame.git"
set :branch, "master"

default_run_options[:pty] = true
ssh_options[:forward_agent] = true

after "deploy", "deploy:cleanup" # keep only the last 5 releases

namespace :deploy do
  %w[start stop restart].each do |command|
    desc "#{command} unicorn server"
    task command, roles: :app, except: {no_release: true} do
      run "/etc/init.d/unicorn_#{application} #{command}"
    end
  end

  task :setup_config, roles: :app do
    sudo "ln -nfs #{current_path}/config/nginx.conf /etc/nginx/sites-enabled/#{application}"
    sudo "ln -nfs #{current_path}/config/unicorn_init.sh /etc/init.d/unicorn_#{application}"
    run "mkdir -p #{shared_path}/config"
    put File.read("config/database.example.yml"), "#{shared_path}/config/database.yml"
    puts "Now edit #{shared_path}/config/database.yml and add your username and password"
  end
  after "deploy:setup", "deploy:setup_config"

  task :symlink_config, roles: :app do
    run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
  end
  after "deploy:finalize_update", "deploy:symlink_config"

  desc "Make sure local git is in sync with remote."
  task :check_revision, roles: :web 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
  before "deploy", "deploy:check_revision"
end

Capfile

load 'deploy'
load 'deploy/assets'
load 'config/deploy'

SSH in to Github to add your keys

# 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

Add ssh key to digitalocean

cat ~/.ssh/id_rsa.pub | ssh -p 1026 deployer@123.123.123.123 'cat >> ~/.ssh/authorized_keys'

Add your Environment variables like ENV["SECRET_KEY"]

sudo nano /etc/environment

On each new line add something like

SECRET_TOKEN=55555DDDDDDDEEEE44444
REDIS_URL=redis://123.123.123.123:6379

Create repo and push to github

# Add config/database.yml to .gitignore
cp config/database.yml config/database.example.yml
git init
git add .
git commit -m "Inital Commit"
git remote add origin git@github.com:gertig/reponame
git push origin master

DEPLOYMENT

cap deploy:setup

Edit database.yml on server to add username and password to the production database

nano apps/projectname/shared/config/database.yml

Deploy it cold

cap deploy:cold

Then remove the nginx default folder (if it doesn't exist don't worry about it)

sudo rm /etc/nginx/sites-enabled/default

Restart Nginx

sudo service nginx restart
sudo update-rc.d -f unicorn_projectname defaults

Now whenever you make changes do the following.

# Make changes
git add .
git commit -m "Changes"
git push origin master

Deploy

cap deploy

Stop and Start the unicorn server

sudo service unicorn_appname stop
sudo service unicorn_appname start

Restart Nginx

sudo service nginx restart

Troubleshooting

Nokogiri

If Nokogiri complains about libxml run this:

sudo apt-get install libxslt-dev libxml2-dev

Then try your cap deploy:cold again if that is where you failed.