Full Stack DevOps

Heroku For Full Stack Deployments

For Feedback Pal I use Heroku to deploy both the Angular frontend and the NestJS API backend.

Let’s start with the frontend.

DevOps for the Angular frontend

There are few activities that need to take place before one can deploy their Angular application on Heroku; that said, it’s not a very complex process.

Create an Heroku account

The first thing to do is to head to heroku.com and create an account. You should enter your credit card details as each application is a payable instance. I spend on average $7 per application per month, so $14 in total.

Create a Heroku application

The next step is to create an Heroku application for the frontend. I called it feedbackpal.

Setting up your project to work with Heroku

An Angular app requires a port where the server listens to (defaults to 4200). The challenge is posed by Heroku automatically assigning a random port to run an application, so a hard-coded value, while works for local development, it doesn’t work with Heroku.

Create a server.js bootstrap file

The first thing I did was to create a bootstrap file. This file is the only file I wrote in JS because it bootstraps an Express server which serves both static content as well as directing all requests to index.html.

const express = require('express');
const path = require('path');
const app = express();
const port = process.env.PORT || 4200;

// Serve static files....
app.use(express.static(__dirname + '/dist/feedbackpal-ui-gen'));

// Send all requests to index.html
app.get('/*', function (req, res) {
  res.sendFile(path.join(__dirname + '/dist/feedbackpal-ui-gen/index.html'));
});

// default Heroku PORT
app.listen(port);
console.log('Server started on port', port);

The code above is really simple. The line that makes the Heroku deployment possible is the following:

const port = process.env.PORT || 4200;

Here we are saying that if there’s an environment variable PORT the bootstrap server will use its value as listening port, otherwise it defaults to 4200.

Next I instruct Express.js to get the static content from the dist/application-name folder and to redirect each requests to index.html so that Angular can manage it.

Modifying the package.json file

The next thing to do is to modify the package.json file to allow for some information that Heroku needs.

"scripts": {
    "ng": "ng",
    "start:dev": "ng serve",
    "start": "node server.js",
    "build": "ng build --configuration=production",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "heroku-prebuild": "npm i -g @angular/cli"
  },
  "engines": {
    "node": "14.x",
    "npm": "7.x"
  },

Here I modified the default start script and call it start:dev to differentiate it from the production start script, which Heroku invokes after deployment to start your application.

The start script executes the server.js file which I described above.

Another change is in the build script which now passes a –configuration=production parameter to activate a production build. As explained in this article, the –configuration=production argument triggers Angular to copy the content of the environment.prod.ts file to environment.ts. As the entire code relies on the content of the environment.ts file, we are sure to always have the right content: either the dev values in the default configuaration, or the production values when the build is triggered with this additional argument.

The other thing to note is this line:

"heroku-prebuild": "npm i -g @angular/cli"

Upon finding this script, Heroku will invoke it as part of the deployment / build cycle. Here we install the Angular CLI on the container running the frontend, so that the subsequent build script finds the required dependencies.

Automated deployments by linking the Heroku App to your GitHub repository

Finally, we instruct Heroku that this is a Node application and about the versions we prefer.

Heroku has introduced a capability that allows developers to link an application to their GitHub repository. By specifying the repository and the branch, as well as selecting auto-deployment, every time one pushes a change to that branch, Heroku will automatically build and deploy the code.

At the end of a successful build and deployment, Heroku will provide the URL of the deployed application. Of course one might want to use custom domains, which require additional resources, so apart from a very basic redirection from https://feedbackpal.techwings.io to https://feedbackpal.herokuapp.com I haven’t set up the full SSL certificate for my domain. The application, however, runs on HTTPS, so it’s fully secure, the only thing being that the URL is normally <application>.herokuapp.com

DevOps for the NestJS API backend

On the backend things are more involved. While the PORT approach remains the same, the API backend needs a handful of environment variables to connect to the database, to interact with Auth0 as well as some configuration to interact with the JWT Token passed in by the Angular frontend.

Setting up a Heroku application for the API backend

Similarly to what we saw for the Angular frontend, before deploying the NestJS API backend to Heroku, one needs to create an application, so go through this process and create one. I called mine feedbackpal-api.

Setting up the API backend environment variables

The API backend requires numerous environment variables to work properly. This is in line with the 12 Factors where one of the guidelines is to create an external configuration that can work in any environment. Environment variables are perfect for this and thankfully Heroku offers the ability to securely set environment variables for your application.

To simplify the setup, I’ve created a little bash script which sets them all up, so I have to invoke the script only once. Let’s look at it, anonymising sensitive data.

heroku config:set DB_HOST='your-db-host' -a feedbackpal-api
heroku config:set DB_PASSWORD='your-db-password' -a feedbackpal-api
heroku config:set DB_USERNAME='your-db-username' -a feedbackpal-api
heroku config:set DB_PORT='25060' -a feedbackpal-api
heroku config:set DB_NAME='feedbackpal' -a feedbackpal-api
heroku config:set AUTH0_AUDIENCE='https://api.feedbackpal.techwings.io' -a feedbackpal-api
heroku config:set AUTH0_DOMAIN='techwings.eu.auth0.com' -a feedbackpal-api
heroku config:set AUTH0_ISSUER_URL='https://techwings.eu.auth0.com/' -a feedbackpal-api
heroku config:set NPM_CONFIG_PRODUCTION='true' -a feedbackpal-api
heroku config:set AUTH0_ADMIN_API_CLIENT_ID='your-auth0-admin-api-client-id' -a feedbackpal-api
heroku config:set AUTH0_ADMIN_API_CLIENT_SECRET='your-auth0-admin-api-secret' -a feedbackpal-api
heroku config:set JWT_NS_PREFIX='https://feedbackpal.techwings.io' -a feedbackpal-api

To setup a Heroku environment variable, the command is:

Heroku config:set ENV_VARIABLE_NAME=ENV_VARIABLE_VALUE -a <application-name>

Setting up the PORT

You might have noticed that we don’t set the PORT environment variable in the list above. If we did that, effectively we would hard-code the PORT value but Heroku automatically assigns a random port to each running app and it automatically makes the PORT environment variable available to the application.

The NestJS API backend is bootstrapped in the main.ts file, as shown below:

import { Inject, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  app.enableCors();
  app.useGlobalPipes(
    new ValidationPipe({
      disableErrorMessages: true,
    })
  );

  const port = process.env.PORT || configService.get('PORT');

  await app.listen(port);
  console.log(`Server listening on port ${port}`);
}
bootstrap();

The code which deals with the PORT is the following:

const port = process.env.PORT || configService.get('PORT');

await app.listen(port);

Here the application either expects the value to be available as an environment variable (for production) or via an entry in the .env file, shown below for completeness:

.env file – Configuration for local development

Here you will notice that we set the PORT environment variable in the .env file. As a reminder, this file is only used for local development, and therefore made available to the ConfigService. However in production we don’t want to set this environment variable: we want to get the value that Heroku provides us with.

Setting up the package.json file

Similarly to what we did for the Angular frontend, we need to modify the package.json file to accommodate for Heroku. Below is the part that is relevant to this.

"scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json",
    "heroku-prebuild": "npm install -g @nestjs/cli"
  },
  "engines": {
    "node": "12.x",
    "npm": "7.x"
  },

Here I did the following:

  • Renamed the default start script as start:dev so that the local development server starts in watch mode for hot redeployments
  • Added a start script, which is the one Heroku will invoke after a successful deployment, to start the NestJS API server.
  • Finally I instructed Heroku on the Node/NPM versions I need for this application, through the engines object property

The Heroku Procfile

The last configuration step before we can connect our Heroku API application to our GitHub repository for automated deployments is to setup a Procfile. As mentioned in the Heroku documentation, the Procfile is used by Heroku to execute whatever command you mention there to startup your application. The Procfile for the Feedback Pal API backend is as follows:

Procfile

Here we are instructing Heroku that to start the application, it should run the command npm run start:prod. This in turn will look into the package.json file and execute the instructions mentioned there:

"start:prod": "node dist/main"

That’s it! With this last step, you’re now ready to connect your application to the GitHub repository and branch you want to automatically deploy and to deploy your API backend.