Wiki

Clone wiki

bashinate / howto-nodeserver

How To: Provisioning a Node Application Server

This how-to will guide you through the process of provisioning a server that will able to host Node.js applications.

NOTE: My recommended configuration for running Node apps in production is behind nginx but that is out of scope for this how-to.

At the end of the guide you will have the following:

  • A single server capable of hosting multiple node applications
  • Git deploy endpoints (with custom post-receive hooks) that handle node application deployment.

Before you begin, you will need:

  • A barebones machine running Ubuntu 12.04 (or similar). I tend to fire one up using either Digital Ocean or Linode.

  • ssh access to that machine. I prefer using authorized_keys validated access (which Digital Ocean makes really easy, BTW) but it's up to you.

Let's get started. Log into the machine using ssh.

In this exercise I'm going to be running as the root user, but if you aren't then remember to sudo as required.

First, download the bashinate BASH script and make it executable:

wget https://bitbucket.org/DamonOehlman/bashinate/raw/master/bashinate -O bashinate
chmod a+x bashinate

Next, we are going to need to create a text file that will provide directives to bashinate (via STDIN) as to what it needs to do.

Using your favourite text editor, create the following system_recipe text file (lines starting with a # are ignored by bashinate):

# tell bashinate that we are ok with an update to Upstart.conf
allow-modify /etc/dbus-1/system.d/Upstart.conf

# install node
node 0.10.22

# set up a holding space for our new node app
nodeapp helloworld 3060

NOTE: The need for the allow systemupdate line is currently being investigated.

Run the script:

./bashinate < system_recipe

This sets bashinate in motion.

As it parses the incoming stream, it is looking for and what I've called "scriptlets". In our example above, we have used three scriplets:

  • the allow scriplet, which we use to tell bashinate core that certain operations are ok. In this case we are giving the thumbs up to the patch operation which in turn will allow us to patch upstart to allow user jobs.

  • the node scriptlet which is used to install a localized version of node on the machine.

  • the nodeapp scriptlet which prepares a container for a node application on the machine.

These scriptlets in turn may invoke other scriptlets by using the require function which is core to bashinate. For example, the nodeapp scriptlet uses the githost scriptlet to bootstrap a git repository on the server.

After a time, provisioning will complete (bashinate tends to compile things like node from source), but there are still a few manual steps that need to be taken care of (automation is on the TODO list):

Firstly, we need to set up the newly provisioned git user with suitable authorized_keys (ref issue). For the purpose of this example, I'm going to copy my currently authorized_keys across for the git user:

cp ~/.ssh/authorized_keys /home/git/.ssh/authorized_keys
chown git:git /home/git/.ssh/authorized_keys

At this point, it's worth opening new terminal window in your local environment also so we can clone a test application repository, and then deploy it to our server.

So in your local environment, clone the nodeapp-helloworld repository:

git clone https://bitbucket.org/DamonOehlman/nodeapp-helloworld.git
cd nodeapp-helloworld

This will clone our simple node helloworld application locally. Feel free to npm install locally and take it for a test drive if you wish. The next thing that we will be doing in the tutorial though is adding our remote deployment endpoint for the repo:

git remote add deploy git@yourhost.tld:apps/helloworld.git

The above line tells our local git repository that we have a remote called deploy that we can now deploy to. Before copying and pasting the above line you will want to replace yourhost.tld with either your target hostname or IP.

Once you have added the remote you should be able to push to the deploy endpoint:

git push deploy master

If everything goes to plan, your application will be pushed to the deploy remote on the master branch. Additionally, the git post-receive hook will kick in and run the required deployment logic (running npm install, restarting the upstart service, etc). The output that is generated should be similar to the following:

Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 729 bytes, done.
Total 5 (delta 0), reused 0 (delta 0)
remote: ~/apps/helloworld/master ~/apps/helloworld.git
remote: Already on 'master'
remote: npm http GET https://registry.npmjs.org/express
remote: npm http 304 https://registry.npmjs.org/express
remote: npm http GET https://registry.npmjs.org/connect/2.9.0
remote: npm http GET https://registry.npmjs.org/commander/1.2.0
remote: npm http GET https://registry.npmjs.org/range-parser/0.0.4
remote: npm http GET https://registry.npmjs.org/mkdirp/0.3.5
remote: npm http GET https://registry.npmjs.org/cookie/0.1.0
remote: npm http GET https://registry.npmjs.org/buffer-crc32/0.2.1
remote: npm http GET https://registry.npmjs.org/methods/0.0.1
remote: npm http GET https://registry.npmjs.org/fresh/0.2.0
remote: npm http GET https://registry.npmjs.org/send/0.1.4
remote: npm http GET https://registry.npmjs.org/cookie-signature/1.0.1
remote: npm http GET https://registry.npmjs.org/debug
remote: npm http 304 https://registry.npmjs.org/range-parser/0.0.4
remote: npm http 304 https://registry.npmjs.org/commander/1.2.0
remote: npm http 304 https://registry.npmjs.org/buffer-crc32/0.2.1
remote: npm http 304 https://registry.npmjs.org/cookie/0.1.0
remote: npm http 304 https://registry.npmjs.org/connect/2.9.0
remote: npm http 304 https://registry.npmjs.org/methods/0.0.1
remote: npm http 304 https://registry.npmjs.org/fresh/0.2.0
remote: npm http 304 https://registry.npmjs.org/send/0.1.4
remote: npm http 304 https://registry.npmjs.org/cookie-signature/1.0.1
remote: npm http 304 https://registry.npmjs.org/debug
remote: npm http 304 https://registry.npmjs.org/mkdirp/0.3.5
remote: npm http GET https://registry.npmjs.org/mime
remote: npm http GET https://registry.npmjs.org/keypress
remote: npm http GET https://registry.npmjs.org/qs/0.6.5
remote: npm http GET https://registry.npmjs.org/bytes/0.2.0
remote: npm http GET https://registry.npmjs.org/pause/0.0.1
remote: npm http GET https://registry.npmjs.org/uid2/0.0.2
remote: npm http GET https://registry.npmjs.org/multiparty/2.1.8
remote: npm http 304 https://registry.npmjs.org/mime
remote: npm http 304 https://registry.npmjs.org/keypress
remote: npm http 304 https://registry.npmjs.org/uid2/0.0.2
remote: npm http 304 https://registry.npmjs.org/multiparty/2.1.8
remote: npm http 304 https://registry.npmjs.org/pause/0.0.1
remote: npm http 304 https://registry.npmjs.org/bytes/0.2.0
remote: npm http 304 https://registry.npmjs.org/qs/0.6.5
remote: npm http GET https://registry.npmjs.org/stream-counter
remote: npm http GET https://registry.npmjs.org/readable-stream
remote: npm http 304 https://registry.npmjs.org/stream-counter
remote: npm http 304 https://registry.npmjs.org/readable-stream
remote: express@3.4.0 node_modules/express
remote: ├── methods@0.0.1
remote: ├── range-parser@0.0.4
remote: ├── cookie-signature@1.0.1
remote: ├── fresh@0.2.0
remote: ├── buffer-crc32@0.2.1
remote: ├── cookie@0.1.0
remote: ├── debug@0.7.2
remote: ├── mkdirp@0.3.5
remote: ├── commander@1.2.0 (keypress@0.1.0)
remote: ├── send@0.1.4 (mime@1.2.11)
remote: └── connect@2.9.0 (uid2@0.0.2, pause@0.0.1, qs@0.6.5, bytes@0.2.0, multiparty@2.1.8)
remote: restart: Unknown instance: 
remote: helloworld start/running, process 1231
remote: ~/apps/helloworld.git
To git@distractable.me:apps/helloworld.git
 * [new branch]      master -> master

Now, if everything has gone to plan you should be able to browse to your application on:

http://yourhost.tld:3060/

Updated