A few weeks ago I bought a new MacMini to replace my PPC MacMini that was pulled out of service a few years ago. It had a few rails and jekyll applications that I ended up moving to my wife’s iMac. Most were just copied and running in developer mode. I had an AppleScript to control start, stop, restart. All worked fine, but not the right way of doing things. I’ll point out that the rails applications are small and are either demo sites for clients or something with a limited audience.

I decided to make another attempt at Capistrano and do it right - at least my version of right. I used as my guide a series of new and revised Railscasts that covered Capistrano and deployment (Deploying to a VPS, Capistrano Task, Capistrano Recipes) My problem was that it was all based on L-unix (sorry, my code word or Linux).

The next few weeks was spend getting these apps ready for deployment, learning more than I wanted about launchd, bash scripting, Mountain Lion, Mountain Lion Server, and a bunch of gems. I went around in circles for a while since I first had to move the apps in pretty much their current state to the server and run under unicorn instead of thin. This while trying to get the cap deployment to work and not step on each other. In the end, the results are not much different than the L-unix version. I did run into a few major stumbling blocks.

  • rvm does not like Mountain Lion
  • launchctl does not like launching rvm gems
  • launchctl does not know about your shell $PATH
  • it is hard to launch something on startup without launchctl
  • don’t use launchclt for service with a daemon, it will fill up your logs!

After wasting a bunch of time trying to get the Mountain Lion MacMini and my Snow Leapord laptop to behave the same, I gave up on rvm on the server and installed ruby 2.0.0 using Homebrew. What I ended up with can be summarized as follows:

  • Nginx is started from /Library/LaunchDaemons
  • A user is set up control everything else (‘developer’ in my case) with automatic login on startup
    • Could use LaunchDaemons but more sudo that I wanted to fool with at the time
  • All applications and static sites are in ~/apps
  • A local git repository was set up in ~/repo for stuff not on github
  • The router (1st generation AirPort in my case) port forwards all port 80 traffic to the MacMini
    • Nginx config does routing to the applications and sites.

Nginx

The nginx installation just following the brew install nginx info. A plist was added to /Library/LaunchDaemons.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>homebrew.mxcl.nginx</string>
	<key>RunAtLoad</key>
	<true/>
	<key>KeepAlive</key>
	<false/>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/local/opt/nginx/sbin/nginx</string>
		<string>-g</string>
		<string>daemon off;</string>
	</array>
	<key>WorkingDirectory</key>
	<string>/usr/local</string>
</dict>
</plist>

Control on nginx is just sending signal to nginx, e.g., sudo nginx -s reload

Unicorn

Unicorn is launched on startup by a launch agent in ~/Library/LaunchAgents. The plist is symbolically linked to the unicorn.rb configuration file in the apps shared directory. The plist must contain a path that includes a path to the unicorn gem/wrapper plus whatever launchctl needs (/etc/paths).

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>EnvironmentVariables</key>
	<dict>
		<key>PATH</key>
		<string>/usr/local/opt/ruby/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
	</dict>
	<key>Label</key>
	<string>apps.ngg.unicorn</string>
	<key>KeepAlive</key>
	<true/>
	<key>ProgramArguments</key>
	<array>
        <string>/usr/local/opt/ruby/bin/unicorn</string>
        <string>-c</string>
        <string>/Users/developer/apps/ngg/shared/config/unicorn.rb</string>
        <string>-E</string>
        <string>production</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
</dict>
</plist>

The unicorn server(s) are controlled by a modified version of Ryan’s version of githubs unicorn_init.sh. Since I have multiple applications, I added an “application” command to the arguments. There are some minor conversions for L-unix to OSX commands and using launchctl instead of service to start, stop and reload the server.

#!/bin/sh

# Modified version of githubs unicorn_init.sh script to control multiple unicorn processes.
# PLIST added for launchctl (and LABEL if you want to use the lunchy gem)
# usage: unicorn_control.sh <application> command

set -e

# Feel free to change any of the following variables for your app:
TIMEOUT=${TIMEOUT-60}
APP_ROOT="/Users/developer/apps/$1"
PID=$APP_ROOT/shared/pids/unicorn.pid
PLIST="/Users/developer/Library/LaunchAgents/apps.$1.unicorn.plist"
LABEL="apps.$1.unicorn"
AS_USER=developer

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 $AS_USER -c "$1"  
  fi
  
}

case "$2" in
start)
  sig 0 && echo >&2 "Already running" && exit 0
  launchctl load $PLIST
  ;;
stop)
  run "launchctl unload $PLIST" && exit 0
  echo >&2 "Not running"
  ;;
force-stop)
  run "launchctl unload $PLIST" && exit 0
  echo >&2 "Not running"
  ;;
restart|reload)
  sig HUP && echo reloaded OK && exit 0
  echo >&2 "Couldn't reload, starting '$PLIST' instead"
  run "launchctl load $PLIST"
  ;;
dump)
  echo $APP_ROOT
  echo $PID
  echo `cat $PID`
  echo $PLIST
  echo $LABEL
  echo $TIMEOUT
  echo $AS_USER
  echo $OLD_PIN
  echo `cat $OLD_PIN`
  echo "Control Command: $1 $2"
  exit 0
  ;;
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 '$PLIST' instead"
  run "launchctl load $PLIST"
  ;;
reopen-logs)
  sig USR1
  ;;
*)
  echo >&2 "Usage: $0 <start|stop|restart|upgrade|force-stop|reopen-logs>"
  exit 1
  ;;
esac

Capistrano

I’m not going to put all my Capistrano recipes here. When I figure out Octopress, I may add them as a download. Since this is not a bare startup server, I commented out most of install tasks. Don’t want to install Postgres over running applications! Other than that, there are just a few modifications. e.g., My demo sites run as a subdomain to my blog, so I added server_name to the deploy.rb cap script for nginx.

require "bundler/capistrano"

load "config/recipes/base"
load "config/recipes/nginx"
load "config/recipes/unicorn"
load "config/recipes/postgresql"
load "config/recipes/check"

server "stevealex.us", :web, :app, :db, primary: true

set :user, "developer"
set :application, "ngg"
set :deploy_to, "/Users/#{user}/apps/#{application}"
set :deploy_via, :remote_cache
set :launch_agents, "/Users/#{user}/Library/LaunchAgents"
set :use_sudo, false
set :server_name,"golfgaggle.com *.golfgaggle.com"
set :scm, "git"
set :repository, "git@github.com:/salex/ngg.git"
set :branch, "master"

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

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

The unicorn.plist was added to the templates to generate the plist that is linked to in LaunchAgents. My unicorn_control.sh is used to control unicorn.

set_default(:unicorn_user) { user }
set_default(:unicorn_pid) { "#{current_path}/tmp/pids/unicorn.pid" }
set_default(:unicorn_config) { "#{shared_path}/config/unicorn.rb" }
set_default(:unicorn_log) { "#{shared_path}/log/unicorn.log" }
set_default(:plist) { "#{shared_path}/config/unicorn.plist" }
set_default(:unicorn_workers, 2)

namespace :unicorn do
  desc "Setup Unicorn initializer and app configuration"
  task :setup, roles: :app do
    run "mkdir -p #{shared_path}/config"
    template "unicorn.plist.erb", plist
    template "unicorn.rb.erb", unicorn_config
    # unicorn_control and launchctl take care of the below commented out lines
    # template "unicorn_init.erb", "/tmp/unicorn_init"
    # run "chmod +x /tmp/unicorn_init"
    # run "#{sudo} mv /tmp/unicorn_init /etc/init.d/unicorn_#{application}"
    # run "#{sudo} update-rc.d -f unicorn_#{application} defaults"
  end
  after "deploy:setup", "unicorn:setup"

  %w[start stop restart upgrade].each do |command|
    desc "#{command} unicorn"
    task command, roles: :app do
      run "/users/developer/apps/unicorn_control.sh #{application} #{command}"
    end
    after "deploy:#{command}", "unicorn:#{command}"
  end
end

This is the first draft of this process and I have to learn a little more about Octopress before I allow comments, make it smaller, etc. Then there is always Yinglish problems, or my version of English - brain says this, fingers type that.