Tuesday, February 24, 2015

Nodejs on Heroku

After hours of effort and suffering, I was able to deploy a Node.js application to Heroku. This post reveals the effort that one has to make in deploying Node.js apps to Heroku.

I had been not using Heroku for some time now and wanted to host a test application on Heroku. This test application has a DB connection to MongoDb, uses the excellent Express Framework in the server side and React with Flux in the Frontend.

The first thing that one immediately notices is that its not that easy to add add-ons in Heroku anymore. They have a policy now with specifying a Credit Card / Payment Details even though I would only like to avail one Dyno. Previously I remember when I could add small add-ons to any Cedar Instance. That was so much cooler. I went on to look at OpenShift from RedHat, but that was a much bigger pain than I expected.

So back to Heroku. The first thing that I tried was to look at MongoLab for a simple Db hosting. They have a nice free developer plan and thats what I went for. The url for the DB went onto the ENV Config for the new Heroku App created. All fine until now.

Now came the actual building process. By default, if one does not specify the node and npm versions in the package.json file, it take the latest versions from semver.io. So for node it took 0.12.0 (https://semver.io/node/stable) and for npm 2.6.0 (https://semver.io/npm/stable).

I have many packages in my test application and it failed compiling for a package called node-sass@1.2.3. This was because of a known issue (https://github.com/sass/node-sass/issues/550) and was fixed with an upgrade of node-sass. But my dependency came from another package and could not upgrade it myself. So I had to tie the version for npm and node.

Once could do this using the engines option inside the package.json file.

"engines": {
  "node": "0.10.x",
  "npm": "2.3.0"
}

The next problem was with the actual building via grunt.

I use wiredep to manage the bower dependencies for me. It injects the dependencies very gracefully and in a fully configurable way. I use font-awesome too in the test app, but it never could add fonts to the dist folder after build. I only way I could do this was via a copy grunt task.

The dependencies are added to the html file (according to the bower:css, bower:js comments) after the build. One normally would like to have the built version served on Heroku. My builds all go to the dist folder and therefore my script start looked like this

"scripts": {
  "start": "NODE_ENV=production node dist/server.js"
}


Once the fonts got copied over successfully, the pending issue was to execute the bower install and grunt build task. This could be done via a post-install hook available with npm. One has just to write something similar to the line below

"scripts": {
  "start": "NODE_ENV=production node dist/server.js",
  "test": "grunt test",
  "postinstall": "bower install && grunt build"
}


The next problem that I faced was that bower and grunt were not a part of the dependencies list. Heroku by default has the setting NPM_CONFIG_PRODUCTION set to true. This means that the npm install only installs the items in the dependencies list and not the devDependencies list (logically). This meant that I either had to move all the grunt tasks and their deps to the dependencies list or set the variable to false. I preferred the latter

heroku config:set NPM_CONFIG_PRODUCTION=false

Now the postinstall was failing. It was not able to find the commands bower and grunt. This was because the commands never got installed globally. To go around this I made a shell file called post_install.sh which has these commands with the expanded paths (according to http://gregtrowbridge.com/deploying-a-node-app-to-heroku-with-grunt-and-bower/)

#!/bin/bash
./node_modules/bower/bin/bower install
./node_modules/grunt-cli/bin/grunt build

and the call from the scripts look like

"scripts": {
  "start": "NODE_ENV=production node dist/server.js",
  "test": "grunt test",
  "postinstall": "bash ./post_install.sh"
}


The grunt-cli was not a part of my original devDependencies list and it had to be added for the command to execute properly.

Now the application ran fine! Phew!

No comments: