Everyone knows engineers can’t write! I
is
an
engineeer!
But...
I Wish I Could Write!
Back to articles
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](http://railscasts.com/episodes/335-deploying-to-a-vps), [Capistrano Task](http://railscasts.com/episodes/133-capistrano-tasks-revised), [Capistrano Recipes](http://railscasts.com/episodes/337-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 <?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 <?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. ```sh #!/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. ```ruby 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. ```ruby 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. .markdown
Rails 3 deploy with nginx unicorn on OSX
March 19, 2013 10:53