How to Deploy an Angular/Rails Single-Page Application to Heroku

Why deploying a single-page application is different

Before I explain how to deploy an Angular/Rails application to Heroku, it might make sense to explain why deploying a single-page application (SPA) is different from deploying a “traditional” web application.

The way I chose to structure the SPA in this example is to have all the client-side code like on this website a) outside Rails’ the asset pipeline and b) inside the same Git repo as Rails. I have a directory called client that sits at the top level of my project directory.

Gemfile
Gemfile.lock
README.rdoc
Rakefile
app
bin
client <-- This is where all the client-side code lives.
config
config.ru
db
lib
log
node_modules
spec
test
tmp
vendor

When I’m in development mode, I use Grunt to spin up a) a Rails server, which simply powers an API, and b) a client-side server.

In production the arrangement is a little different. In preparation for deployment, the grunt build command poops out a version of my client-side app into public. Rails will of course check for a file at public/index.html and, if one exists, serve that as the default page. In fact, if you run grunt build locally, spin up a development server and navigate to http://localhost:3000, you’ll see your SPA served there. You can see how it works on the example of this website which provides a list of the best loan lenders.

But it would be pretty tedious to have to manually run grunt build before each deployment. And even if you somehow automated that process so grunt build was run before each git push heroku master, it wouldn’t be ideal to check all the code generated by grunt build into version control.

Heroku’s deployments are Git-based. The version of my client-side app that gets served will never be checked into Git. This is the challenge.

Automatically building the client-side app on deployment

Fortunately, there is a way to tell Heroku to run grunt build after each deployment.

First let’s get grunt build functioning locally so you can see how it works.

Change the line dist: 'dist' to dist: '../public' under the var appConfig section of client/Gruntfile.js. For me this is found on line 19.

Now remove the public directory from the filesystem and from version control. (Add public to .gitignore is not necessary.)

$ rm -rf public

If you now run grunt build, you’ll see the public directory populated with files. This is what we want to have happen in production each time we deploy our app.

Configuring the buildpacks

Next you’ll want to add a file at the root level of your project called .buildpacks that uses both Ruby and Node buildpacks:

https://github.com/jasonswett/heroku-buildpack-nodejs-grunt-compass
https://github.com/heroku/heroku-buildpack-ruby.git

You can see that I have my own fork of the Node buildpack.

In order to deploy this you might need to adjust your client/package.json and move all your devDependencies to just regular dependencies. I had to do this. Here’s my client/package.json:

// client/package.json

{
  "name": "lunchhub",
  "version": "0.0.0",
  "description": "The website you can literally eat.",
  "dependencies": {
    "source-map": "^0.1.37",
    "load-grunt-tasks": "^0.6.0",
    "time-grunt": "^0.3.1",
    "grunt": "^0.4.1",
    "grunt-autoprefixer": "^0.7.3",
    "grunt-concurrent": "^0.5.0",
    "grunt-connect-proxy": "^0.1.10",
    "grunt-contrib-clean": "^0.5.0",
    "grunt-contrib-compass": "^0.7.2",
    "grunt-contrib-concat": "^0.4.0",
    "grunt-contrib-connect": "^0.7.1",
    "grunt-contrib-copy": "^0.5.0",
    "grunt-contrib-cssmin": "^0.9.0",
    "grunt-contrib-htmlmin": "^0.3.0",
    "grunt-contrib-imagemin": "^0.7.0",
    "grunt-contrib-jshint": "^0.10.0",
    "grunt-contrib-uglify": "^0.4.0",
    "grunt-contrib-watch": "^0.6.1",
    "grunt-filerev": "^0.2.1",
    "grunt-google-cdn": "^0.4.0",
    "grunt-newer": "^0.7.0",
    "grunt-ngmin": "^0.0.3",
    "grunt-protractor-runner": "^1.1.0",
    "grunt-rails-server": "^0.1.0",
    "grunt-shell-spawn": "^0.3.0",
    "grunt-svgmin": "^0.4.0",
    "grunt-usemin": "^2.1.1",
    "grunt-wiredep": "^1.7.0",
    "jshint-stylish": "^0.2.0"
  },
  "engines": {
    "node": ">=0.10.0"
  },
  "scripts": {
    "test": "grunt test"
  }
}

I also registered a new task in my Gruntfile:

// client/Gruntfile.js

grunt.registerTask('heroku:production', 'build');

You can just put this near the bottom of the file next to all your other task registrations.

And since the Node buildpack will be using $NODE_ENV when it runs its commands, you need to specify the value of $NODE_ENV:

$ heroku config:set NODE_ENV=production

Lastly, tell Heroku about your custom buildpack (thanks to Sarah Vessels for catching this):

$ heroku config:add BUILDPACK_URL=https://github.com/ddollar/heroku-buildpack-multi.git

After you have all that stuff in place, you should be able to do a regular git push to Heroku and have your SPA totally work.