Currently we're building a backend system to support the customer care and sales department of our client. This week the users came up with the requirement to have real-time notifications of data changes. Such stuff generally sounds like a lot of fun and the chance to drop in some top-notch technology. Thus we decided to build this notification system on top of Node.js and Socket.IO.
The Node server acts as a simple relay server for JSON payloads sent from the backend system which is a Rails 3 app. The JSON payload is then broadcast to the connected Socket.IO clients (i.e. the user's browser). The implementation is really simple and not worth further mentioning it. But I'm a big fan of deploying a system as early as possible. This is especially true for systems introducing new technologies to an existing infrastructure which was eventually the case here. Due to this it is the deployment that challenged me and this is how I solved it.
Installing Node.js and npm on the server
First things first. Before we could even think about deploying our brand new real time notifcation system on a server, we need the basic software installed: Node and npm. Both was installed via Chef. We used the nodejs package from the Opscode cookbooks repository and added a dead simple npm recipe:
include_recipe "nodejs"
execute "install_npm" do
command "curl http://npmjs.org/install.sh | sudo sh"
user "root"
group "root"
not_if "which npm"
end
So far, so good – that was the easy part. Now for the real fun ...
Capistrano isn't just for Rails
Being a full time Rails developer for while now the word Deployment fires almost immediately an association with Capistrano. The main advantage of using Capistrano (or any other weapon of your choice) is consistency in usage. For me this is maybe one of the most important goal to achieve, from writing code itself down the way of managing a server.
With Capistrano the only task you have to care about is how to control your server process – which would be a Node.js process in this case. It turns out controlling such a bitch isn't as easy as controlling an Unicorn app server for instance. It means in detail there is actually no way to detach the Node server process but to use standard operating system methods:
nohup node server_script.js &
Unfortunatley, this doesn't really help. Of course, I could have used some other tools provided by the OS like init scripts or, as mentioned in this article, upstart. But I like the idea of keeping the deployment details for my apps at one place and not to spill one part in the app repo and the other in my Chef scripts. OK, but that demanded another way of controlling my little Node server.
Meet Cluster
On my quest for salvation I finally found the Cluster package which turned out
to do the job just good enough. I still have to start the Node process
through the above mentioned nohup construct but now I a PID-file
was generated and a CLI interface for restarting and stopping the process was available. Even a
status command is available but only with version 0.4.1 or higher of
Node.js.
Wrapping it all together in the three Capistrano task deploy:start,
deploy:stop and deploy:restart I ran in just another problem:
Running the nohup command directly via Capistranos run method isn't
working out: No error is raised but also there is no server up
and running. I didn't fully understand the underlying problem but I
assume it had something to do with the pseudo-TTY Capistrano allocates.
The solution to the problem was as simple as encapsulating the start
command in a separate Rake-task.
But 'nuff said, here is the corresponding Capistrano code:
namespace :deploy do
desc "Restarting the Node.js process"
task :restart, :roles => :app, :except => { :no_release => true } do
run <<-SHELL
if [ -f #{shared_path}/pids/master.pid ]; \
then cd #{current_path} && node push_server.js restart; \
fi && cd #{current_path} && rake server:start
SHELL
end
desc "Starting the Node.js process"
task :start, :roles => :app do
run "cd #{current_path} && rake server:start"
end
desc "Stopping the app Node.js process"
task :stop, :roles => :app do
run <<-SHELL
cd #{current_path} && node push_server.js shutdown && \
rm #{shared_path}/pids/*.pid
SHELL
end
end
Bonus round: Managing NPM dependencies
As I said, I prefer keeping the app stuff together in one place. And being spoiled with Bundlers dependency management I wanted at least to define the npm dependencies for the app in the repository. I used a simple Rake task to get a script installing the dependencies for free:
desc "Install npm dependencies"
task :dependencies do
npm_dependencies = [
"express",
"socket.io",
"cluster"
]
npm_dependencies.each do |dep|
system "sudo npm install #{dep}"
end
end
The only flaw of this solution is the use of sudo which is the new
recommended way of installing npm packages since 0.3.0.
This isn't really a problem on a local dev machine but it is quite annoying
when your deployment user doesn't have any sudo privileges. Due to
this first I have to update the code on the remote machine to have the
new list of dependencies available and then to log into that machine to run the Rake
task with a privileged user. On the other hand the current app is too
simple and too small to justify a more sophisticated solution ;-)
Reminder: You definitely want to use a process monitoring solution like monit or god to take care of your pretty Node.js processes. But that is a completley different story.
