Home

Nate Wienert

Developer, Designer. Creator of 2u.fm, GameGum & obtvse.

Rubinius, Puma, chruby, Capistrano and Nginx on a DigitalOcean Droplet

What an adventure! Seriously, look at that mouthful. On the tail end of spending two full days moving 2u.fm onto this setup with hours of hair-pulling (yay!) so here’s a quick recap to hopefully save you some trouble.

First you need chruby. chruby is an extremely lightweight ruby switching tool. Going from RVM to rbenv to chruby feels like moving from a Escalade to a Camry to a Lotus Elise. It’s slightly less cushy, but faster and leaner (and therefore easier to debug and less finicky). You should be able to install it easily from their instructions.

I decided to go full bore and blow away rbenv on my local machine as well so I would be using the same thing on both ends. If you are on a mac you can do that with:

brew install chruby

Now, I ran into some trouble building Rubinius using ruby-build (see this ticket on github) but my bet is typically it would work. Nonetheless for the server, it is probably best to build from source if you want an edge version anyway as it is easier to debug. Lets also install clang and use that (clang is generally faster and uses less memory than gcc):

1
2
3
4
5
apt-get install clang
git clone git://github.com/rubinius/rubinius.git
cd rubinius
./configure --prefix=/opt/rubies/rbx-2.0.0-master --cc=clang --cxx=clang++
rake install

chruby checks in /opts/rubies for your ruby version by default. On my server (and local box) I use zsh so I put this in my ~/.zshrc (or ~/.bashrc for bash users):

1
2
3
source /usr/local/share/chruby/chruby.sh
source /usr/local/share/chruby/auto.sh
chruby rbx

This installs chruby, the auto line will change rubies as you cd around, and the last line sets your default ruby to Rubinius (and is not required).

Now set up the git repo and website directory:

1
2
3
4
git init --bar /var/git/app_name.git
mkdir /var/www/app_name/web
mkdir /var/www/app_name/web/shared
mkdir /var/www/app_name/web/shared

My Capistrano config/deploy.rb has important bits: …

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# ssh forwarding and shell
set :default_run_options, { :pty => true }
set :ssh_options, { :forward_agent => true }
...
# chruby
# set :bundle_flags,  "--verbose"
set :ruby_version, "rbx"
set :chrub_script, "/usr/local/share/chruby/chruby.sh"
set :set_ruby_cmd, ". #{chrub_script} && chruby #{ruby_version}"
set(:bundle_cmd) { "#{set_ruby_cmd} && RAILS_ENV=#{rails_env} exec bundle" }
# Roles
role :web, domain
role :app, domain
role :db,  domain, :primary => true # This is where Rails migrations will run
after 'deploy:update_code', 'deploy:migrate'
after 'deploy:update', 'deploy:symlink_attachments'
after 'deploy:update', 'deploy:symlink_tmp'
after 'deploy:update', 'deploy:clear_caches'
after 'deploy:update', 'deploy:cleanup'
# Run rake tasks
def run_rake(task, options={}, &block)
  command = "cd #{latest_release} && #{bundle_cmd} rake #{task}"
  run(command, options, &block)
end
namespace :puma do
  task :start, :except => { :no_release => true } do
    run "/etc/init.d/puma start #{application}"
  end
  after "deploy:start", "puma:start"
  task :stop, :except => { :no_release => true } do
    run "/etc/init.d/puma stop #{application}"
  end
  after "deploy:stop", "puma:stop"
  task :restart, roles: :app do
    run "/etc/init.d/puma restart #{application}"
  end
  after "deploy:restart", "puma:restart"
end
namespace :deploy do
  task :symlink_attachments do
    run "ln -nfs #{shared_path}/attachments #{release_path}/public/attachments"
  end
  task :symlink_tmp do
    run "rm -rf #{release_path}/tmp"
    run "ln -nfs #{shared_path}/tmp #{release_path}/tmp"
    run "chmod 775 #{shared_path}/tmp"
  end
  task :clear_caches do
    run "echo 'flush_all' | nc localhost 11211" # memcached
    run_rake "tmp:cache:clear >/dev/null 2>&1"
  end
end

… and the config/puma.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env puma
basedir = '/var/www/APP_NAME/web/current'
directory "#{basedir}"
environment 'production'
daemonize true
bind "unix://#{basedir}/tmp/puma/puma.sock"
pidfile "#{basedir}/tmp/puma/pid"
state_path "#{basedir}/tmp/puma/state"
threads 4, 48
preload_app!
activate_control_app

Notes:

  • Make sure #{basedir}/tmp/puma/ is there, but don’t create any files inside it
  • Make sure the account that runs deploy.rb/puma is the owner of your web directory and shared directory
  • Turn off daemonizing and socket for debugging

I had to add chruby to the init.d script. It’s massive, so here’s a gist.

Also some changes for /usr/local/bin/run-puma:

1
2
3
#!/bin/zsh
app=$1; config=$2; log=$3;
source /usr/local/share/chruby/chruby.sh && chruby rbx && cd $app && puma --debug -C $config 2>&1 >> $log

Finally, nginx should connect like normally:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
upstream APP_puma {
  server unix:///var/www/APP/web/current/tmp/puma/puma.sock;
}
server {
  listen 80 default;
  server_name APP.com www.APP.com;
  root /var/www/APP/web/current/public;
  location ~ ^/(assets|fonts|images|attachments)/?|favicon.ico  {
    root /var/www/APP/web/current/public;
    expires max;
    break;
  }
  location / {
    proxy_set_header   Host             $host;
    proxy_set_header   X-Real-IP        $remote_addr;
    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
    proxy_redirect off;
    proxy_pass http://APP_puma;
  }
}

To see all the files in full, check out this gist.