How to Deploy an Angular 2/Rails 5 App to Heroku

by Jason Swett,

Here’s how to deploy an Angular 2 and Rails 5 app to Heroku. There’s more than one possible way this sort of thing could be structured. Before I describe how to set up the deployment I’ll explain how I chose to structure my app and why.

First, my app is a single-page application where the front-end code is completely separate from the back-end API code. The alternative would be to use Rails views and include the JavaScript and CSS in the asset pipeline. I don’t think either way is necessarily better than the other. In this case I chose the SPA way because a) I understand a lot of people are structuring their Angular/Rails apps this way and b) between the two structures, it’s probably the trickier of the two to deploy, and so it’s probably the one that could benefit most from being documented.

I could have chosen to keep my Angular app and my Rails app in two separate repos. In that case the Rails code could have been deployed to Heroku and the Angular code could have been deployed to, say, a static AWS site. What I instead chose to do was to include my Angular app in a subdirectory of the Rails project and host it all on the same Heroku app. The reason I did it this way is because a) front-end and back-end changes are often made at the same time by the same people and b) although it might have been less work initially to host the Angular app and the Rails app in two different places, it’s arguably simpler from a day-to-day workflow standpoint just to have a single way to deploy everything to one place.

The deployment steps below begin with the creation of an Angular/Rails app, so you don’t have to worry about having your own app to deploy. These steps will get you all the way there but you can also clone my example repo if you don’t want to follow the steps manually.

Initializing the App

The first step will be to initialize a new Rails 5 project. I’m calling mine rails_5_angular_2_deployment_example.

For the front-end we’ll use angular2-seed. Clone the repo into a subdirectory called client. (The name client, in this case, is important. If you call it something else, the deployment won’t work.)

git clone git@github.com:mgechev/angular2-seed.git client

We’re just interested in the angular2-seed code. If we keep it as a repository inside our project’s repository, there will be problems. Kill client/.git.

rm -rf client/.git

Now install the Node packages.

cd client
npm install

Before we try to deploy our app we’ll want to make sure it works locally. Run the following command to do a build.

npm run build.prod

This command will create a directory called client/dist/prod. If we want Rails to be able to serve something out of there, we’ll have to tell Rails about it somehow. We can do this by simply symlinking public/ to client/dist/prod/. Rails will look for public/index.html and find client/dist/prod/index.html and we’ll be in business.

cd ..
rm -rf public
ln -s client/dist/prod public

Start the Rails server. If you navigate to localhost:3000 you should see “Howdy! Here’s a list of awesome computer scientists…”

rails server

Creating the Heroku App

The first step is somewhat self-explanatory:

heroku create

We’ll need to use two buildpacks for this deployment. If we were deploying a plain old Rails app, Heroku would auto-detect Ruby and use the heroku/ruby buildpack. We also need a Node buildpack because in order to build our front-end app we need to run a Gulp command and in order to run Gulp commands we need to have Node packages installed.

We can tell Heroku about our two buildpacks like this:

heroku buildpacks:add https://github.com/jasonswett/heroku-buildpack-nodejs
heroku buildpacks:add heroku/ruby

The order matters. If we were to put the Ruby buildpack first and the Node buildpack second, Heroku would give us a single Node dyno which wouldn’t know how to run Ruby. By putting the Ruby buildpack second we get two dynos: a web dyno and a worker dyno, both of which know how to run Ruby.

The reason we’re using my https://github.com/jasonswett/heroku-buildpack-nodejs buildpack instead of the Heroku version is that I needed to modify the buildpack to look for package.json inside of the client directory instead of at the project root.

We need to do one last thing before we can push our code. Modify client/package.json to look like this:

{
  "name": "angular2-seed",
  "version": "0.0.0",
  "description": "Seed for Angular 2 apps",
  "repository": {
    "url": "https://github.com/mgechev/angular2-seed"
  },
  "scripts": {
    "build.dev": "gulp build.dev --color",
    "build.dev.watch": "gulp build.dev.watch --color",
    "build.e2e": "gulp build.e2e --color",
    "build.prod": "gulp build.prod --color",
    "build.test": "gulp build.test --color",
    "build.test.watch": "gulp build.test.watch --color",
    "docs": "npm run gulp -- build.docs --color && npm run gulp -- serve.docs --color",
    "e2e": "protractor",
    "e2e.live": "protractor --elementExplorer",
    "gulp": "gulp",
    "karma": "karma",
    "karma.start": "karma start",
    "postinstall": "typings install && gulp check.versions && npm prune && gulp build.prod",
    "reinstall": "npm cache clean && npm install",
    "serve.coverage": "remap-istanbul -b src/ -i coverage/coverage-final.json -o coverage -t html && npm run gulp -- serve.coverage --color",
    "serve.dev": "gulp serve.dev --color",
    "serve.e2e": "gulp serve.e2e --color",
    "serve.prod": "gulp serve.prod --color",
    "start": "gulp serve.dev --color",
    "tasks.list": "gulp --tasks-simple --color",
    "test": "gulp test --color",
    "webdriver-start": "webdriver-manager start",
    "webdriver-update": "webdriver-manager update"
  },
  "author": "Minko Gechev <mgechev>",
  "license": "MIT",
  "devDependencies": {
  },
  "dependencies": {
    "angular2": "2.0.0-beta.15",
    "es6-module-loader": "^0.17.8",
    "es6-promise": "^3.1.2",
    "es6-shim": "0.35.0",
    "reflect-metadata": "0.1.2",
    "rxjs": "5.0.0-beta.2",
    "systemjs": "~0.19.25",
    "zone.js": "^0.6.10",
    "async": "^1.4.2",
    "autoprefixer": "^6.3.3",
    "browser-sync": "^2.11.2",
    "chalk": "^1.1.3",
    "codelyzer": "0.0.12",
    "colorguard": "^1.1.1",
    "connect": "^3.4.1",
    "connect-history-api-fallback": "^1.1.0",
    "connect-livereload": "^0.5.3",
    "cssnano": "^3.5.2",
    "doiuse": "^2.3.0",
    "event-stream": "^3.3.2",
    "express": "~4.13.1",
    "express-history-api-fallback": "^2.0.0",
    "extend": "^3.0.0",
    "gulp": "^3.9.1",
    "gulp-cached": "^1.1.0",
    "gulp-concat": "^2.6.0",
    "gulp-filter": "^4.0.0",
    "gulp-inject": "^4.0.0",
    "gulp-inline-ng2-template": "^1.1.2",
    "gulp-load-plugins": "^1.2.0",
    "gulp-plumber": "~1.1.0",
    "gulp-postcss": "^6.1.0",
    "gulp-shell": "~0.5.2",
    "gulp-sourcemaps": "git+https://github.com/floridoo/gulp-sourcemaps.git#master",
    "gulp-template": "^3.1.0",
    "gulp-tslint": "^4.3.3",
    "gulp-typedoc": "^1.2.1",
    "gulp-typescript": "~2.12.1",
    "gulp-uglify": "^1.5.3",
    "gulp-util": "^3.0.7",
    "gulp-watch": "^4.3.5",
    "is-ci": "^1.0.8",
    "isstream": "^0.1.2",
    "jasmine-core": "~2.4.1",
    "jasmine-spec-reporter": "^2.4.0",
    "karma": "~0.13.22",
    "karma-chrome-launcher": "~0.2.2",
    "karma-coverage": "^0.5.5",
    "karma-ie-launcher": "^0.2.0",
    "karma-jasmine": "~0.3.8",
    "karma-mocha-reporter": "^2.0.0",
    "karma-phantomjs-launcher": "^1.0.0",
    "merge-stream": "^1.0.0",
    "open": "0.0.5",
    "phantomjs-prebuilt": "^2.1.4",
    "postcss-reporter": "^1.3.3",
    "protractor": "^3.0.0",
    "remap-istanbul": "git+https://github.com/SitePen/remap-istanbul.git#master",
    "rimraf": "^2.5.2",
    "run-sequence": "^1.1.0",
    "semver": "^5.1.0",
    "serve-static": "^1.10.2",
    "slash": "~1.0.0",
    "stream-series": "^0.1.1",
    "stylelint": "^5.3.0",
    "stylelint-config-standard": "^5.0.0",
    "systemjs-builder": "^0.15.14",
    "tiny-lr": "^0.2.1",
    "traceur": "^0.0.91",
    "ts-node": "^0.7.1",
    "tslint": "^3.7.0-dev.2",
    "tslint-stylish": "2.1.0-beta",
    "typedoc": "^0.3.12",
    "typescript": "~1.8.10",
    "typings": "^0.7.12",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^1.1.0",
    "yargs": "^4.2.0"
  }
}

We’ve done two things here. First, we added gulp build.prod to the postinstall script. This will force Heroku to do a build as part of the deployment process. Second, we move everything from devDependencies to dependencies. Since it’s a production environment, Node won’t pick up the devDependencies, but we need those.

By the way, why not just build locally, commit the generated code, and push that? You could do that and it would work. The reason I didn’t want to is that it’s not a good idea to commit build artifacts to version control. You end up with a bunch of “changed” files every time you do a build, which not only doesn’t make sense but serves as a distraction. I’ve worked on projects before that commit build artifacts to version control and it has been painful.

After all our changes are committed we can do a push:

git push heroku master

Now open the app in Heroku.

heroku open

You should see the same Angular 2 Seed app in the production environment. Obviously, the Angular app isn’t talking to Rails but it is coexisting with it, and that’s the hard part.

3 thoughts on “How to Deploy an Angular 2/Rails 5 App to Heroku

  1. Nmuta Jones

    When you do it this way, you’re essentially telling Rails: “hey …. route all traffic to client/dist/prod/index.html, which you ( Rails ) thinks is public/index.html. This is fine, and it works, but what if you intend to build an SPA with Angular and you need to use Angular routing ? Rails is still handling the routing here. If , in your routes file, you put nothing, all routes going to / would hit the Angular path, but you could not do /books or something like that , because the routes hit Rails first and Rails doesn’t understand the /books route.

    You could do a catch all , like this:
    get ‘*path’, to: ‘pages#angular_app’

    but then the Angular app is living as a page within the Rails routing system.

    How are you guys solving this problem ? Have you tried running the full SPA with routing within Rails?

    Reply
    1. nmac

      My workaround is to switch location strategy to HashLocationStrategy. Anything after the hash (#) isn’t sent to the server so you just get redirected by Rails to root, which is the Angular page. From there, the Angular app processes the path defined after the hash and navigates to that page.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *