Deploying node.js on Amazon EC2

Ben Lindsey ·

After nearly a month of beating my head against the wall that is hosted node.js stacks — with their fake beta invites and non-existent support — I decided it was time to take matters into my own hands. Amazon Web Service (AWS) offers 12 months of a micro instance for free (as in beer) with 10 GB of disk and 613 MB of memory. This is perfect for an acceptance server running node. All you need to do is sign up with a new email address and provide a credit card. Totally worth it. After 12 months, the price will jump to roughly $15 a month.

I’m a huge fan of Debian and it’s progeny Ubuntu. The guys over at http://www.alestic.com/ do a great job of providing Amazon Machine Images (ami) that are production ready. I choose to use Ubuntu 10.04 LTS because it will be supported until April of 2015. The 64 bit ami for the us-east region is ami-63be790a. Feel free to choose one that best suits your needs.

You will want to setup your default Security Group (the AWS firewall) to allow inbound port 22 and 80 at a minimum (read this for more information). After your instance is up and running, download the ssh identity file (*.pem) and ssh to your new server as the ubuntu user.

ssh -i <identity>.pem ubuntu@ec2-127-0-0-1.compute-1.amazonaws.com

At this point you will want to update your package list and perform all the upgrades for security purposes.

$ sudo apt-get update
$ sudo apt-get upgrade -y

Shortly we can get down to the business at hand but first we need to install a few build tools.

$ sudo apt-get install build-essential libssh-dev git-core -y

Then we can download and install node (takes about 20 minutes to build):

$ wget http://nodejs.org/dist/node-v0.4.11.tar.gz
$ tar zxf node-v0.4.11.tar.gz
$ cd node-v0.4.11
$ ./configure
$ sudo make install

Finally we can download and install the node package manager (npm).

$ curl http://npmjs.org/install.sh | sudo sh

Now your instance is ready to run a node.js server (node server.js). Yes it is really that easy.

Deployment

We use teamcity to run all of our tests and automatically deploy to our acceptance server with capistrano. This means you will need a ruby installed as well as a few gems. Bring on the Gemfile:

source 'http://rubygems.org'

gem 'capistrano'
gem 'capistrano-ext'
gem 'bluepill'


Then run a bundle to install all the required gems.

Here is the Capfile:

load 'deploy' if respond_to?(:namespace) # cap2 differentiator
load 'config/deploy' # remove this line to skip loading any of the default tasks

And the capistrano file config/deploy.rb (change it to your EC2 hostname and github repo):

set :stages, %w(acceptance production)
require 'capistrano/ext/multistage'

set :application, "node"
set :user, "ubuntu"
set :host, "ec2-127-0-0-1.compute-1.amazonaws.com"
set :deploy_to, "/var/www/node"
set :use_sudo, true

set :scm, :git
set :repository, "git@github.com:your/repo.git"
set :branch, "development"

set :deploy_via, :remote_cache
role :app, host

set :bluepill, '/var/lib/gems/1.8/bin/bluepill'

default_run_options[:pty] = true

namespace :deploy do
  task :start, :roles => :app, :except => { :no_release => true } do
    run "#{try_sudo :as => 'root'} #{bluepill} start #{application}"
  end

  task :stop, :roles => :app, :except => { :no_release => true } do
    run "#{try_sudo :as => 'root'} #{bluepill} stop #{application}"
  end

  task :restart, :roles => :app, :except => { :no_release => true } do
    run "#{try_sudo :as => 'root'} #{bluepill} restart #{application}"
  end

  task :create_deploy_to_with_sudo, :roles => :app do
    run "#{try_sudo :as => 'root'} mkdir -p #{deploy_to}"
  end

  task :npm_install, :roles => :app, :except => { :no_release => true } do
    run "cd #{release_path} && npm install"
  end
end

before 'deploy:setup', 'deploy:create_deploy_to_with_sudo'
after 'deploy:finalize_update', 'deploy:npm_install'


Note that this assumes bluepill is installed as a system gem. And here is the stage file config/deploy/acceptance.rb (don’t forget to add your EC2 hostname):

set :node_env, 'acceptance'
set :branch, 'development'
set :keep_releases, 10

server 'ec2-127-0-0-1.compute-1.amazonaws.com', :web, :app, :db, :primary => true


You will need to setup inbound and outbound ssh keys for github and teamcity. Add your teamcity id_rsa.pub to /home/ubuntu/.ssh/authorized_keys on the EC2 server and copy your teamcity id_rsa to /home/ubuntu/.ssh/id_rsa as well.

For process management I decided to try out bluepill because I’ve found monit to be unruly and I’m sorta in like with ruby. Here is the acceptance.pill:

Bluepill.application("app") do |app|
  app.process("node") do |process|
    process.working_dir = "/var/www/node/current"
    process.start_command = "/usr/bin/env NODE_ENV=acceptance app_port=80 node server.js"
    process.pid_file = "/var/www/node/shared/pids/node.pid"
    process.stdout = process.stderr = "/var/www/node/shared/log/node.log"
    process.daemonize = true

    process.start_grace_time = 10.seconds
    process.stop_grace_time = 10.seconds
    process.restart_grace_time = 20.seconds

    process.checks :cpu_usage, :every => 10.seconds, :below => 5, :times => 3
    process.checks :mem_usage, :every => 10.seconds, :below => 100.megabytes, :times => [3,5]
  end
end


Now we just need to load up the configuration on the server:

$ bluepill load acceptance.pill

You can run a cap acceptance deploy from your dev machine or your CI server and the new code will go out and your node.js process will be restarted. You might have to tweak your authorized_keys to make that happen though. Enjoy.

UPDATE: The alestic AMI ships with no swap space so if you are running memory intensive apps you might want to check here: http://www.cyberciti.biz/faq/linux-add-a-swap-file-howto/

Ben Lindsey
Ben Lindsey

Runner; Chef; Trancer; Traveler; Technologist; Agile evangelist. I'm a firm believer in that which you measure will surely improve. Currently I am working on how to test drive and scale a node.js application in the cloud.