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.