Tips for node.js deployment on SmartOS
At the lovely company I work, we’re doing quite some work with node.js. Best environment for node.js deployment is a SmartOS machine, of course. One of the nice features of SmartOS is the deep level of dtrace integration. Check Max’s presentation from Node Dublin ‘12.
On the other side, SmartOS is SunOS’s younger nephew and not Linux’, which means that all your init.d/upstart scripts your already have will not be 100% portable to this new OS.
I’ll show here a couple of tips, to make the first deployment of your node.js service or site easier.
First, give your server a nicer name, so when you ssh in, it’s easier on your eyes and memory then the default md5/guid name.
echo "massive-lannister.local" > /etc/nodename
Second prepare your server to communicate with your Github account. Generate a private key…
ssh-keygen -t rsa
cat ~/.ssh/id_rsa.pub
…and copy the content of ~/.ssh/id_rsa.pub to GitHub > Account Settings > SSH Keys > Add SSH key.
Now let’s set up a home for your project.
mkdir /home/massive/
cd /home/massive/
Clone the repository
git clone git@github.com:bamse/massive-lannister.git
Install the npm dependencies
cd massive-lannister/
npm install
To set up the node.js service, you’ll be using Service Management Facility.
In your git project, create a directory srvconf where the you’ll store the non-critical server configuration files like nginx.conf or SMF manifest.
Now create a SMF xml manifest based on this template.
<?xml version="1.0"?>
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
<service_bundle type="manifest" name="node-massive-lannister-service">
<service name="site/node-massive-lannister-service" type="service" version="1">
<create_default_instance enabled="true"/>
<single_instance/>
<dependency name="network" grouping="require_all" restart_on="refresh" type="service">
<service_fmri value="svc:/milestone/network:default"/>
</dependency>
<dependency name="filesystem" grouping="require_all" restart_on="refresh" type="service">
<service_fmri value="svc:/system/filesystem/local"/>
</dependency>
<method_context working_directory="/home/massive/massive-lannister">
<method_credential user="admin" group="staff" privileges='basic,net_privaddr' />
<method_environment>
<envvar name="PATH" value="/home/massive/massive-lannister/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin"/>
<envvar name="HOME" value="/home/massive/massive-lannister"/>
<envvar name="NODE_ENV" value="production"/>
</method_environment>
</method_context>
<exec_method
type="method"
name="start"
exec="/opt/local/bin/node /home/massive/massive-lannister/app.js"
timeout_seconds="60"/>
<exec_method
type="method"
name="stop"
exec=":kill"
timeout_seconds="60"/>
<property_group name="startd" type="framework">
<propval name="duration" type="astring" value="child"/>
<propval name="ignore_error" type="astring" value="core,signal"/>
</property_group>
<property_group name="application" type="application">
</property_group>
<stability value="Evolving"/>
<template>
<common_name>
<loctext xml:lang="C">node.js massive lannister service</loctext>
</common_name>
</template>
</service>
</service_bundle>
Import the service and start it.
svccfg import srvconf/node-massive-lannister-service-manifest.xml
svcadm enable node-massive-lannister-service
And verify that it works:
tail -f -n 50 "/var/svc/log/site-node-massive-lannister-service:default.log"
If you wish to stop the service:
svcadm disable node-massive-lannister-service
If you’re using the node.js service behind a nginx server, set this up too. Some companies (like Joyent) are exposing node.js directly, not behind a proxy.
Install nginx:
pkgin in nginx -y
Copy the nginx config:
mkdir -p /opt/local/etc/nginx/sites-enabled
cp srvconf/nginx-site-api.conf /opt/local/etc/nginx/sites-enabled
Edit nginx.conf and add the include line after http {
vim /opt/local/etc/nginx/nginx.conf
#add this line after http {
include /opt/local/etc/nginx/sites-enabled/*.conf;
Enable nginx:
svcadm enable nginx
Sample of nginx-massive-lannister.conf
server {
listen 80; ## listen for ipv4; this line is default and implied
#listen [::]:80 default ipv6only=on; ## listen for ipv6
root /home/massive/massive-lannister/public;
index index.html index.htm;
# Make site accessible from http://localhost/
server_name api.massive-lannister.com;
location /api {
proxy_pass http://127.0.0.1:5648;
proxy_buffering off;
proxy_connect_timeout 66;
proxy_read_timeout 66;
proxy_send_timeout 66;
proxy_hide_header X-Powered-By;
proxy_hide_header X-Response-Time;
proxy_set_header Host $host;
}
}
To make a new deployment, at a later point:
ssh root@massive-lannister.com
cd /home/massive/massive-lannister/
git pull
svcadm restart node-massive-lannister-service
Another useful tip, with this setup is the separation of production and development environment. If you noticed, in the manifest xml file, we set a variable called NODE_ENV to production.
In app.js, include the reference to your config file:
var conf = require('./lib/config.js')
And later:
seneca.use('mongo-store', conf.mongo)
The content of lib/config.js is pretty small:
var env = process.env.NODE_ENV || 'development'
console.log('Config in ' + env + ' mode')
var config = {}
config.port = 5648; //TYRI
if (env === 'development') {
config.mongo = {
name:'db-dev',
host:'',
username:'massive-test',
password:''
}
} else {
config.mongo = {
name:'db-prod',
host:'',
username:'massive-prod',
password:''
}
}
module.exports = config
Inspired by Isaac Schlueter’s Github project.
Happy coding!
P.S. In case you’re wandering who’s massive-lannister, that’s the name of the project suggested by Github, when I went on to create a new repository. Some of their suggestions were north-american-hipster and mustached-sansa. If Tyrion Lannister and Sansa are present, I guess that someone from Github is a big fan of George RR Martin’s A Song of Ice and Fire. Or HBO’s Game of Thrones, I can’t tell until I see if they used Khaleesi or Daenerys for the Mother of Dragons’ name.