My nodeJS deployment approach

A while back I said I wasn’t really satisfied with how I was deploying node applications, in this case Codeshelver. After dealing with the problem for some more time I’ve now arrived at a more general solution that I’d like to share with you. Having a strong Ruby background some of this will seem familiar to you Ruby developers: I’m deploying to a nginx server, the node process gets monitored with monit and the basic deployment stack includes Vlad as a deployment tool, a ndistro file that contains the app dependencies (think of it as a Gemfile) and a self-written start/stop script for my node applications (with a little Express flavor).

Rakefile

This is a standard Rakefile that just loads Vlad and sets it to use git for source control.

require 'rake'

begin
  require 'vlad'
  Vlad.load(:app => nil, :type => nil, :scm => 'git')
rescue LoadError
  puts "Could not load Vlad - please run 'gem install vlad vlad-git'"
end

deploy.rb

This is Vlad’s config file that contains some configuration as well as some recipes I wrote for ndistro and the deployment process:

set :user, "deploy"
set :application, "myapp"
set :domain, "myapp.com"
set :deploy_to, "/var/www/#{application}"
set :repository, "git@github.com:myuser/#{application}.git"

namespace :vlad do
  # overrides
  set :shared_paths, { }
  set :mkdirs, []

  namespace :ndistro do
    desc "Install ndistro"
    remote_task :setup, :roles => :web do
      set :use_sudo, true
      run "cd /usr/local/bin && curl https://github.com/fairwinds/ndistro/raw/master/install | sh"
    end

    desc "Install dependencies"
    remote_task :install_deps, :roles => :web do
      %w(bin lib modules).each do |dir|
        shared_dir_path = File.join(shared_path, dir)
        release_dir_path = File.join(current_release, dir)
        run "mkdir -p #{shared_dir_path}"
        run "ln -s #{shared_dir_path} #{release_dir_path}"
      end
      run "cd #{current_release} && ndistro"
    end

    desc "Clean dependencies"
    remote_task :clean_deps, :roles => :web do
      %w(bin lib modules).each do |dir|
        shared_dir_path = Filec.join(shared_path, dir)
        run "rm -r #{shared_dir_path}"
      end
    end
  end

  desc 'Start the app'
  remote_task :start_app, :roles => :app do
    run "/etc/init.d/express_app #{application} start"
  end

  desc 'Stop the app'
  remote_task :stop_app, :roles => :app do
    run "/etc/init.d/express_app #{application} stop"
  end

  desc 'Restart the app'
  remote_task :restart_app, :roles => :app do
    %w(stop_app start_app).each { |task| Rake::Task["vlad:#{task}"].invoke }
  end

  desc "Full deployment cycle: Update, ndistro:install_deps, restart, cleanup"
  remote_task :deploy, :roles => :app do
    %w(update ndistro:install_deps restart_app cleanup).each do |task|
      Rake::Task["vlad:#{task}"].invoke
    end
  end
end

.ndistro

The ndistro file contains the module and node dependencies of your app. This one is pretty simple - here you can read up more on ndistro and there also is a short screencast about it.

module senchalabs connect
module visionmedia express
module visionmedia expresso
module visionmedia haml.js
module visionmedia sass.js

express_app

This is the script I use to start/stop the server, I keep it under /etc/init.d/ so that I can use it like any other system script. You use it like this: /etc/init.d/express_app {app_name} {start|stop}

#!/bin/bash
user=deploy
dir=/var/www/$1/current
srv_file=$dir/server.js
pid_file=$dir/log/server.pid
log_file=$dir/log/server.log

case $2 in
  start)
    sudo -u "$user" EXPRESS_ENV=production node "$srv_file" >> "$log_file" 2>&1 &
    echo `ps ax | grep $srv_file | grep -v grep | awk '{print $1}'` > "$pid_file"
    chown "$user":"$user" "$log_file"
    chown "$user":"$user" "$pid_file" ;;
  stop)
    kill `cat "$pid_file"`
    rm "$pid_file" ;;
  *)
    echo "usage: /etc/init.d/express_app {app_name} {start|stop}" ;;
esac
exit 0

monit

This is pretty simple as it just keeps an eye on the server pid file and utilizes the start/stop script to keep the app running.

check process myapp with pidfile /var/www/myapp/current/log/server.pid
  start program = "/etc/init.d/express_app myapp start"
  stop program  = "/etc/init.d/express_app myapp stop"

nginx.conf

Last but not least here’s an excerpt from the nginx config file:

http {
  upstream myapp_cluster {
    server localhost:3000;
  }

  server {
    listen 80;
    server_name myapp.com;
    root /var/www/myapp/current;

    location / {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_pass http://myapp_cluster/;
      proxy_redirect off;
    }
  }
}

I hope this helps you figuring out your deployment and maybe you got some tips for me on how to enhance this. Of course there are many other valid approaches like using upstart or with an Apache as webserver.

Wondering whether Node.js is the right choice for you? Please also read Why The Hell Would I Use Node.js? on the Toptal Engineering blog - it is a a case-by-case tutorial, which goes into great detail and will give you the insights you need to approach the decision.

iPhone app for GitHub

iOctocat

is GitHub in your pocket: The go to app for staying up to date with your projects on your iPhone, and iPod Touch.
It is