TL;DR: This 8-part tutorial series covers building and deploying a full-stack JavaScript application from the ground up with hosted MongoDB, Express, Angular (v2+), and Node.js (MEAN stack). The completed code is available in the mean-rsvp-auth0 GitHub repo and a deployed sample app is available at https://rsvp.kmaida.net. Part 8 of the tutorial series covers NgModule refactoring, lazy loading, and production deployment on VPS with nginx and SSL.


Real-World Angular Series

You can view all sections of the tutorial series here:

  1. Real-World Angular Series - Part 1: MEAN Setup & Angular Architecture
  2. Real-World Angular Series - Part 2: Authentication and Data Modeling
  3. Real-World Angular Series - Part 3: Fetching and Displaying API Data
  4. Real-World Angular Series - Part 4: Access Management, Admin, and Detail Pages
  5. Real-World Angular Series - Part 5: Animation and Template-Driven Forms
  6. Real-World Angular Series - Part 6: Reactive Forms and Custom Validation
  7. Real-World Angular Series - Part 7: Relational Data and Token Renewal
  8. Real-World Angular Series - Part 8: Lazy Loading, Production Deployment, SSL (congratulations, you are here!)

Part 8: Lazy Loading, Production Deployment, SSL

The seventh part of this tutorial covered deleting events, retrieving relational data from MongoDB to list events a user has RSVPed to, and silently renewing authentication tokens.

The eighth and final installment in the series covers NgModule refactoring, lazy loading, and production deployment on VPS with nginx and SSL.

  1. Angular: Refactor NgModules
  2. Angular: Lazy Loading
  3. Intro to Deploying a MEAN App
  4. Digital Ocean Setup
  5. Set Up SSL
  6. Deploy Application on Digital Ocean
  7. Production Auth0 Settings
  8. Conclusion

Angular: Refactor NgModules

Let's pick up right where we left off last time. Angular uses NgModules to organize an application into cohesive blocks of functionality. We currently have just one NgModule: our root module, called AppModule. To improve our app's organization and enable lazy loading, we're going to do some refactoring by adding additional NgModules.

Let's start the NgModules refactor by creating modules for our src/app/auth and src/app/core directories. This will start to help clean up our app.module.ts file.

Create Auth Module

Create a module with the Angular CLI like so:

$ ng g module auth

This command generates an auth.module.ts file in our existing auth folder. Let's open this file and make the following updates:

// src/app/auth/auth.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthService } from './auth.service';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [],
  providers: [
    AuthService
  ]
})
export class AuthModule { }

This module now makes the AuthService provider available to our application when imported.

Create Core Module

Now we'll create a core module the same way:

$ ng g module core

Open the new core.module.ts file and add:

// src/app/core/core.module.ts
import { NgModule } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DatePipe } from '@angular/common';
import { ApiService } from './api.service';
import { UtilsService } from './utils.service';
import { FilterSortService } from './filter-sort.service';
import { SubmittingComponent } from './forms/submitting.component';
import { LoadingComponent } from './loading.component';
import { HeaderComponent } from './../header/header.component';
import { FooterComponent } from './../footer/footer.component';

@NgModule({
  imports: [
    CommonModule,
    HttpClientModule,
    RouterModule,
    FormsModule,
    ReactiveFormsModule
  ],
  declarations: [
    HeaderComponent,
    FooterComponent,
    LoadingComponent,
    SubmittingComponent
  ],
  providers: [
    Title,
    DatePipe,
    ApiService,
    UtilsService,
    FilterSortService
  ],
  exports: [
    HttpClientModule,
    RouterModule,
    FormsModule,
    ReactiveFormsModule,
    HeaderComponent,
    FooterComponent,
    LoadingComponent,
    SubmittingComponent
  ]
})
export class CoreModule { }

We'll move HTTP and forms modules to our core module, as well as the title service and necessary features in the src/app/core directory and our header and footer. We'll import the appropriate modules, services, and components. We also need to add RouterModule in order to support the Header component's navigation directives. We'll add the modules to the NgModule's imports array, the components to the declarations array, and the services to the providers array. Then we'll also need to export the modules and components if we want to be able to use them in the components of other NgModules.

Note: We are not moving the BrowserAnimationsModule. This is because this module is in @angular/platform-browser. Because BrowserModule needs to be imported in the app module, all ancillary modules from @angular/platform-browser need to be imported in the same module. Otherwise, we will get a BrowserModule has already been loaded error when we implement lazy loading.

Our core module is fairly substantial, but collects the shared features of our application in one place. They're no longer mixed in with all of our app's other components.

Update App Module

We need to add our new modules to our app module, as well as clean up the imports we moved. Open the app.module.ts file and modify it like so:

// src/app/app.module.ts
// @TODO: remove auth and core imports/declarations
...
import { AuthModule } from './auth/auth.module';
import { CoreModule } from './core/core.module';
...
@NgModule({
  ...
  imports: [
    ...
    AuthModule,
    CoreModule
  ],
  ...

We'll import the two new modules we created at the top of the file, and also add them to the imports array in the @NgModule(). We also need to clean up the app.module.ts file so we don't have duplicates of anything; this will cause compiler errors. Make sure all the imports and references to moved features have been removed.

Note: Your IDE's intellisense and any Angular compiler errors in the browser should help you clean this up fairly easily.

You'll notice that most of the remaining imports in our app module (with a few exceptions) are now page components. The file looks much cleaner and more manageable. Now we will create a few more modules to manage features.

Create Event Module

Our Event component has several child components, including event details, RSVPs, and the RSVP form. Let's create a module for this page as a whole.

$ ng g module pages/event

Open the event.module.ts file and add:

// src/app/pages/event/event.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from './../../core/core.module';
import { EventComponent } from './event.component';
import { EventDetailComponent } from './event-detail/event-detail.component';
import { RsvpComponent } from './rsvp/rsvp.component';
import { RsvpFormComponent } from './rsvp/rsvp-form/rsvp-form.component';

@NgModule({
  imports: [
    CommonModule,
    CoreModule
  ],
  declarations: [
    EventComponent,
    EventDetailComponent,
    RsvpComponent,
    RsvpFormComponent
  ]
})
export class EventModule { }

We'll need to import the CoreModule we created earlier in order for our event components to access its exports. We'll then import all of our event components and add them to the declarations array.

Create Admin Module

Now we'll do the same with our Admin component and its children.

Create the new module:

$ ng g module pages/admin

Open the admin.module.ts file and add the following:

// src/app/pages/admin/admin.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from './../../core/core.module';
import { AdminComponent } from './admin.component';
import { CreateEventComponent } from './create-event/create-event.component';
import { UpdateEventComponent } from './update-event/update-event.component';
import { EventFormComponent } from './event-form/event-form.component';
import { DeleteEventComponent } from './update-event/delete-event/delete-event.component';

@NgModule({
  imports: [
    CommonModule,
    CoreModule
  ],
  declarations: [
    AdminComponent,
    CreateEventComponent,
    UpdateEventComponent,
    EventFormComponent,
    DeleteEventComponent
  ]
})
export class AdminModule { }

Like in our event module, we'll import CoreModule and all the components associated with our admin page.

Update App Module

Now it's time to update app.module.ts again. We'll delete all the imports and references that we moved. Then we'll import the two new modules we created:

// src/app/app.module.ts
// @TODO: remove event and admin component imports/declarations
...
import { EventModule } from './pages/event/event.module';
import { AdminModule } from './pages/admin/admin.module';
...
@NgModule({
  ...
  imports: [
    ...
    EventModule,
    AdminModule
  ],
  ...

Note: After we verify that refactored modules work, we'll be removing these imports in favor of lazy loading them instead.

Our app should continue to function the way it always did, but we have a cleaner architecture in place. We're now ready to implement lazy loading for our event and admin routes.


Angular: Lazy Loading

Currently, all of our routes are eagerly loaded. This means all routes are compiled in the same bundle, which loads on initialization of our single page app (SPA) in the browser. It's often a better approach to only load routes when they're needed (lazy loading). With this approach, bundle chunks are compiled as separate JavaScript files. Each chunk is only loaded when the user navigates to the route that needs that code.

"Use NgModules to implement lazy loading with Angular."

Lazy loading in Angular relies on modules. In addition to gaining improved architecture, we also get the ability to easily add lazy loading as a result of creating an EventModule and AdminModule. Instead of loading components with our routes, we'll use loadChildren with a string pointing to the proper NgModule.

Note: You could lazy load all routes in your application by adding NgModules for each route. We'll only lazy load the event and admin routes in this tutorial, but feel free to add more lazy loading in your apps.

Let's implement lazy loading!

Lazy Load Event Route

We'll lazy load our event module first. The event feature is a substantial amount of code and functionality, so we'll load it on demand. We can implement lazy loading in three simple steps.

The first thing we need to do is move routing for our event component to the EventModule. We'll create a new file that exports an EVENT_ROUTES constant. Make a new file in src/app/pages/event called event.routes.ts:

// src/app/pages/event/event.routes.ts
import { Routes } from '@angular/router';
import { EventComponent } from './event.component';

export const EVENT_ROUTES: Routes = [
  {
    path: '',
    component: EventComponent
  }
];

This is a child route of our existing 'event/:id' route, hence the empty path (it inherits its route from the 'event/:id' parent). The parent 'event/:id' in our app-routing.module.ts will become componentless and will reference the event NgModule instead of the EventComponent.

Let's import the EVENT_ROUTES constant into our event.module.ts and implement it:

// src/app/pages/event/event.module.ts
...
import { RouterModule } from '@angular/router';
import { EVENT_ROUTES } from './event.routes';

@NgModule({
  imports: [
    ...,
    RouterModule.forChild(EVENT_ROUTES)
  ],
  ...
})
...

We'll import the RouterModule and the EVENT_ROUTES constant. Then we'll import RouterModule and set forChild(), passing the event routes we just created.

Now open the app-routing.module.ts:

// src/app/app-routing.module.ts
// @TODO: remove EventComponent import
...

const routes: Routes = [
  ...,
  {
    path: 'event/:id',
    loadChildren: './pages/event/event.module#EventModule',
    canActivate: [
      AuthGuard
    ]
  },
  ...
];

...

First we'll remove the EventComponent import. Then we'll replace the component: EventComponent in our 'event/:id' route with the following:

loadChildren: './pages/event/event.module#EventModule',

This is a string referencing the path to our event.module.ts and the module's name, EventModule. The code will then look in the event module for routes and load them on demand.

Lazy Load Admin Routes

Now we'll lazy load the admin routes. This is a set of routes rather than a single one, unlike our event route. Lazy loading the admin routes together is useful because any user who logs in who is not an admin will never need to download the admin components. However, if the user is an admin, we can load all the admin routes together the first time they access an admin page. The implementation is the same as above, so let's begin.

Create a new file that exports an ADMIN_ROUTES constant. Make a new file in src/app/pages/admin called admin.routes.ts:

// src/app/pages/admin/admin.routes.ts
import { Routes } from '@angular/router';
import { AdminComponent } from './admin.component';
import { CreateEventComponent } from './create-event/create-event.component';
import { UpdateEventComponent } from './update-event/update-event.component';

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    component: AdminComponent,
  },
  {
    path: 'event/new',
    component: CreateEventComponent
  },
  {
    path: 'event/update/:id',
    component: UpdateEventComponent
  }
];

This is the same as the children array from the app-routing.module.ts.

Now implement these routes in the admin.module.ts:

// src/app/pages/admin/admin.module.ts
...
import { RouterModule } from '@angular/router';
import { ADMIN_ROUTES } from './admin.routes';

@NgModule({
  imports: [
    ...,
    RouterModule.forChild(ADMIN_ROUTES)
  ],
  ...
})
...

Lastly, update the app-routing.module.ts:

// src/app/app-routing.module.ts
// @TODO: remove all admin route component imports
...

const routes: Routes = [
  ...,
  {
    path: 'admin',
    loadChildren: './pages/admin/admin.module#AdminModule',
    canActivate: [
      AuthGuard,
      AdminGuard
    ]
  },
  ...
];

...

Remove the admin component imports (AdminComponent, CreateEventComponent, and UpdateEventComponent). Then replace the children array with loadChildren instead.

Remove Event and Admin Modules From App Module

Now that our event and admin modules are set up to be loaded lazily, we no longer need to import them in our root AppModule. Instead, they will be loaded on demand.

Open the app.module.ts file and delete the import statements for EventModule and AdminModule. In addition, delete these modules from the NgModule's imports array.

Production Build to Verify Lazy Loading

Lazy loading is now implemented! Visit the app in the browser. Everything should continue to work as expected. Verify that you can access all the routes without errors.

We can see that everything works, but in order to really see our lazy loading in action, we need to create a production build. This will use Ahead-of-Time (AOT) compilation instead of Just-in-Time (JIT). It will split our bundles into separate chunks the way we expect for lazy loading.

Stop both the Angular CLI server and the Node server. Then run:

$ ng build --prod
$ node server

The ng build --prod command will create a /dist folder containing our compiled production Angular app. Running node server without the NODE_ENV=dev environment variable will serve our application from Node, running the API while also serving the Angular front end from the production /dist folder.

Note: AOT is the default compilation for production builds in the Angular CLI as of v1.0.0-beta.28.

We can now access our production build at http://localhost:8083. The app should load and run as expected.

Note: Silent token renewal will produce errors on a production build if you don't update the redirect URLs in the silent.html file. However, this is actually the perfect opportunity to test token renewal error handling. You may want to decrease the token expiration again in your Auth0 API to trigger a renewal attempt that will fail due to improper configuration. Then you can ensure that your app handles silent authentication errors gracefully and as expected.

Once you have the app running on http://localhost:8083, open the browser developer tools to the Network tab. Then navigate through the app, keeping an eye on which resources are loaded.

Note: You can change the Network output to JS instead of All to make it easier to see the bundles that are being loaded.

On initial load of an eagerly loaded route such as the homepage, the network panel should look something like this:

Network panel lazy loading eager route

On first load, there are several JS files here. Notice there is a main...bundle.js file. This is the JS for our app module, associated code, and eagerly loaded routes. If we hadn't implemented lazy loading, there wouldn't be any additional bundle loads appearing as long as we continue using our single page app in this session.

However, if you navigate from the homepage to a lazy loaded route such as an event details page, you should see a ...chunk.js file loaded, like so:

Angular lazy loaded route network

This is JavaScript loaded on demand for our lazy loaded route. Click through the app while paying attention to the network panel to verify that lazy loading is functioning properly. The first admin route visited should load one more chunk that covers all admin pages. If the user isn't an admin, that code will never burden the network by loading needlessly.

If everything is working as we expect, it's time to get ready to deploy our application!


Intro to Deploying a MEAN App

We now have a production build of our RSVP application! The obvious next step is to deploy it. Let's take a look at the requirements for production deployment.

VPS Hosting

We can't use shared hosting for our application. We're going to be running a Node server, so a VPS (Virtual Private Server) is our best bet. Fortunately, there are robust and affordable options.

In this tutorial, we will deploy our app to a DigitalOcean VPS. We'll talk about this in more detail shortly.

Note: Heroku is another option, but requires that things be done a specific way. Because of this, if Heroku is your intended production platform, it's strongly recommended that you follow a Heroku-specific MEAN tutorial instead.

MongoDB

You can also consider upgrading your mLab account where the MongoDB is hosted. If the free Sandbox plan will not be sufficient for your production app, you can upgrade to a larger plan. If you're only looking to develop and sandbox, the current free plan should be fine.

Alternatively, you could host your MongoDB locally on your VPS. If it grows large, it will take up space on the server, but will be located in the same place as your app. You can find instructions on how to set up MongoDB on DigitalOcean here.

Domain Name

You'll need a domain name to associate with your VPS so that you can access your app in the browser and secure it with SSL. If you need to register a domain name, you can do so through any number of registrars. Personally, I like to use namecheap.

Deployment Summary

Let's do a quick summary of the steps we need to take in order to deploy our app to production:

  • Spin up an Ubuntu droplet on DigitalOcean and set up the server
  • Install Node
  • Prep our app for VPS deployment via Git
  • Clone and set up our project on VPS
  • Keep app running with pm2
  • Set up a domain name with VPS (either use one you already own or register a new one)
  • Get SSL certificate with Let's Encrypt
  • Install nginx and configure with domain and SSL
  • Update Auth0 Client settings for production

There are a quite a few steps here and we'll be relying on a few tutorials from DigitalOcean's community to get going.

"MEAN deployment checklist: Ubuntu VPS, domain name, SSL certificate."

Let's begin now!


Digital Ocean Setup

The first thing we need is a place to host our app. In this tutorial, I'll show you how to deploy to DigitalOcean.

Create an Ubuntu Digital Ocean Droplet

Create a new droplet on Digital Ocean. Signing up with this link will issue a $10 credit to your new account. If you choose the $10 plan, that's one month free, or two months free on the $5 plan!

Go through the registration steps. When you create your new droplet, choose the Ubuntu 16.04 distribution image:

Digital Ocean Ubuntu distribution image

Note: Ubuntu 16.04.2 x64 is the default at the time of writing. This may change over time. If so, make sure you find applicable tutorials for the version of Ubuntu you chose.

Initial Ubuntu Server Setup

Once your droplet is set up, select it in your DigitalOcean dashboard and follow the steps here: Initial Server Setup with Ubuntu 16.04.

This tutorial instructs you on how to create a new user with sudo privileges. This is the user you should always log in with after initial setup (never as root). The tutorial also shows you how to Add Public Key Authentication and set up a basic firewall.

Add a Host Domain

In order to easily access your app in the browser and secure it with an SSL certificate, you'll need to add a host domain name in the Digital Ocean dashboard. If you've already registered a domain, you can set it up by following the How To Set Up a Host Name with DigitalOcean tutorial.

I am using kmaida.net as my host. I also created a subdomain rsvp.kmaida.net CNAME record for my RSVP app. After setup, my DigitalOcean droplet's Domains look like this:

DigitalOcean DNS records

After following the above tutorial, yours should look similar.

Install Node.js and PM2

Now it's time to install some dependencies on our VPS. The first thing we'll need for our app is Node.js.

Please use the following tutorial to install Node on your DigitalOcean VPS: How To Install Node.js on Ubuntu 16.04.

Note: Using the PPA or nvm method will allow you to install a more recent version of Node beyond the distro-stable version for Ubuntu. Installing via PPA or nvm is recommended if you need a newer version. Using PPA, I installed the latest stable version at the time of writing, which is Node v8.1.0.

After installing Node (and with it, npm), we can install PM2, a Node production process manager. PM2 will enable us to keep our app process running, and to restart it automatically if anything disrupts it.

Install PM2 globally on your VPS with the following command:

$ sudo npm install pm2 -g

You can read more about using PM2 in this tutorial: How To Set Up a Node.js Application for Production on Ubuntu 16.04 - Install PM2.

Install Nginx

The next step is to install nginx. We will use nginx as a reverse proxy to direct client requests to our MEAN stack app.

Side Note: Around the web, you may see various capitalizations of nginx/Nginx versus NGINX. Outside of the logo (which is always the same), the casual use of capitalization often has to do with the type of distribution. When denoted with lowercase letters, "nginx" (or sometimes "Nginx") generally refers to the open-source distribution. NGINX in uppercase letters frequently refers to NGINX Plus, the commercial application delivery platform.

However, to make things more confusing, the website for the open-source version of nginx says "nginx", whereas the website for the commercial platform refers to both as "NGINX" (differentiating the two using the word "Plus").

Use this tutorial to install nginx on your VPS: How To Install Nginx on Ubuntu 16.04.

Note: In Step 2: Adjust the Firewall, enable Nginx Full, since we're going to set up SSL and we want to redirect traffic from HTTP to HTTPS.


Set Up SSL

Since our app deals with user authentication and potentially sensitive information, we want to secure it with SSL, an encrypted link between the browser and web server. In order to do this, we'll need to acquire an SSL certificate. We'll do this with Let's Encrypt, a free certificate authority.

There are several steps involved, so let's begin.

Install Certbot

On our VPS, we'll install the tools that Let's Encrypt needs in order to acquire and manage certificates:

$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install certbot

The tool we need to obtain certificates from Let's Encrypt is called certbot. These commands will install it and its dependencies. Throughout the installation, you will be prompted a few times to proceed.

Obtain a Certificate

Let's obtain a certificate now with the following command:

# e.g., sudo certbot certonly --standalone -d kmaida.net -d www.kmaida.net -d rsvp.kmaida.net
$ sudo certbot certonly --standalone -d [example.com] -d [rsvp.example.com]

The certbot certonly command obtains a certificate but does not assume anything about the software that serves our content. We'll be using nginx, but we'll configure it manually. Because our Node app runs on a localhost webserver and not from the ~/var/www directory, we'll use the --standalone flag.

Make sure you replace the domains in square brackets ([example.com], etc.) with all the domains and subdomains you wish the certificate to include. You may be prompted to enter an admin email and accept terms. Once certbot is finished obtaining your certificate, you should see a Congratulations! message that looks something like this:

Let's Encrypt certbot successfully obtained SSL cert

Automatically Renew Certificate

SSL certificates from Let's Encrypt expire every 90 days. To avoid having to manually renew the certificate every three months, we can set up a cron job to do this for us automatically.

To create a cron job, run this command:

$ sudo crontab -e

Then enter the following:

# Automatically renew certificate
46 0,12 * * * /usr/bin/certbot renew --quiet --renew-hook "/usr/local/bin/systemctl reload nginx"

This schedules certbot to check to see if the certificate is close to expiring twice a day at 12:46 AM and PM. Certbot recommends that the auto-renewal check runs at a random minute within the hour twice a day. If the certificate is close to expiration, certbot will renew it. The --quiet flag silences all output except errors. Upon successful renewal (--renew-hook), nginx is reloaded. If the certificate is not due for renewal, nothing will happen.

Note: You should double-check where your certbot and systemctl executables are located to ensure that the local paths in your cron job are correct. You can do this by running which [cmd], like so:

$ which certbot # e.g., /usr/bin/certbot
$ which systemctl # e.g., /usr/local/bin/systemctl

If necessary, update the paths in the cron code with your appropriate paths.

When finished, use Ctrl + x to exit the nano editor. You'll be prompted to confirm your changes, so enter y to accept. You should receive a message confirming a new crontab was installed.

Create Diffie-Hellman Group

To further secure our connection, let's create a Diffie-Hellman group. This is a method of securely exchanging cryptographic keys over a public channel.

We can create a DH group with the following command:

$ sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

It will take a little while for the DH to be generated. We'll add the generated file to our nginx SSL configuration shortly.

Note: You can learn more about the Diffie-Hellman key exchange here.

Nginx SSL Configuration

Let's create a file containing the SSL configuration for nginx:

$ sudo nano /etc/nginx/snippets/ssl-params.conf

Add the following code in the new ssl-params.conf file:

# /etc/nginx/snippets/ssl-params.conf
# cipherli.st
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

# Diffie-Hellman group
ssl_dhparam /etc/ssl/certs/dhparam.pem;

The first section is comprised of strong SSL security settings from cipherli.st's nginx example. In the code above, the resolver parameter is set to Google's DNS resolvers (8.8.8.8 and 8.8.4.4).

It's important to note that we also changed the X-Frame-Options to SAMEORIGIN. This is because Auth0's token renewal takes place in an iframe. If we don't allow the /silent callback to open in an iframe, we cannot renew authentication silently for our app.

Last, we'll add the Diffie-Hellman ssl-params.conf file we created above. Exit this file and confirm that changes should be saved.

Congratulations! We're now set up to use SSL. We'll do a little more configuration in the next section when we deploy our app.


Deploy Application on Digital Ocean

We're in the home stretch now: deployment! There are a couple of minor things we'll need to do before we deploy our application.

Update Auth Config and Rebuild

We need to update our auth config file for production and rebuild the app to accommodate our production domain.

Open auth.config.ts and make the following change:

// src/app/auth/auth.config.ts
...
  SILENT_REDIRECT: `${ENV.BASE_URI}/silent`,
...

The app will now use the proper redirect to silently renew tokens on our production domain.

Since we made a change to our Angular files, we'll need to do another production build. Now run:

$ ng build --prod

The production bundles will be rebuilt and the /dist folder contents updated with our change.

Don't Ignore Dist Folder

Our production app lives in the /dist directory in our project folder. By default, the Angular CLI has a .gitignore file that excludes this directory from source control. However, we need this folder to be in the Git repo if we want to avoid installing the Angular CLI on the server. This is very easy to fix.

In the .gitignore file in the project root, simply comment out /dist:

# .gitignore
...
# compiled output
# /dist
...

Commit this change. This also allows us to commit our production /dist folder.

Important Note: Make sure your repo is located somewhere that is accessible to your VPS server through Git, such as on GitHub or BitBucket. Alternatively, you can clone the app from the sample repo for this tutorial series, which is located at https://github.com/auth0-blog/mean-rsvp-auth0. If you clone the app from the sample repo, make sure you update the files indicated in the repo's README and still follow these production deployment steps.

Update Silent File

We also need to update our silent.html file with our secure production URL, like so:

<!-- silent.html -->
<!doctype html>
...
  <script>
    ...
    const URL = 'https://[YOUR_DOMAIN]';
    ...
  </script>
...

Change the URL constant that is used in the redirectUri and the postMessage() URL to your domain on HTTPS. This will prevent same origin errors on production when tokens are silently renewed.

Commit this change and push to your Git repository's remote.

Clone RSVP App on DigitalOcean VPS

You should have already set up a superuser for your VPS in the initial server setup tutorial from DigitalOcean. Now open a terminal and connect to the server. Once you're connected, clone the repo that your app lives in and install its Node dependencies, like so:

$ ssh [superuser]@[DigitalOcean_server_IP]
$ cd /
# Clone your own repo, or the sample one below:
$ sudo git clone https://github.com/auth0-blog/mean-rsvp-auth0.git
$ cd mean-rsvp-auth0
$ sudo npm install

Note: Git should already be installed on your DigitalOcean droplet. If it isn't, you can install it like so:

$ sudo apt-get update
$ sudo apt-get git

If you're so inclined, you can clean up the app so that only the production files are present. (Or you can modify the .gitignore locally to exclude the /app folder.) A final option, of course, is to leave the entire app as it is. In production, it will only access the server files and the /dist contents.

Update App Configuration

As it is, the app is missing some backend configuration. The original config.js should never be checked into source control if the repo is available publicly. Therefore, we now need to recreate our server/config.js file with the appropriate information for the app.

On the server, execute the following commands:

$ cd server
$ sudo nano config.js # in this file, paste your config.js contents

Save and exit with Ctrl + x and then y.

Note: If you cloned the sample repo, there is a config.js.SAMPLE file which can be renamed and modified like so:

$ cd server
$ sudo mv config.js.SAMPLE config.js
$ sudo nano config.js # in this file, make changes to reflect your config.js contents

Configure Nginx for the App

We already set up encryption with an SSL certificate from Let's Encrypt. Now it's time to set up the reverse proxy with nginx for our application.

We're going to use an individual configuration file for our RSVP app, so first we'll disable the default file. We can do this with the following commands:

$ cd /etc/nginx/sites-enabled
$ sudo mv default default.bak

Now we can create a new file to store our app's nginx server blocks. Name the file after your full domain with .conf as an extension, like so:

$ sudo nano /etc/nginx/conf.d/[YOUR_DOMAIN].conf # e.g., rsvp.kmaida.net.conf

Configuration files in the conf.d folder are already imported in the /etc/nginx/nginx.conf file, so it's a great way to keep our configurations organized, especially if we'll have additional domains on our VPS in the future.

We'll then set up our new [YOUR_DOMAIN].conf file to redirect all traffic to HTTPS and use our SSL certificate:

# /etc/nginx/conf.d/[YOUR_DOMAIN].conf
server {
  listen 80;
  listen [::]:80;
  server_name [YOUR_DOMAIN];
  return 301 https://$server_name$request_uri;
}
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name [YOUR_DOMAIN];

  # SSL certificate
  ssl_certificate /etc/letsencrypt/live/[YOUR_CERTIFICATE]/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/[YOUR_CERTIFICATE]/privkey.pem;

  # SSL configuration from cipherli.st
  include snippets/ssl-params.conf;

  location / {
    proxy_pass http://localhost:8083/;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-NginX-Proxy true;
    proxy_set_header Host $http_host;
    proxy_ssl_session_reuse off;
    proxy_cache_bypass $http_upgrade;
    proxy_redirect off;
  }
}

Make sure you've replaced [YOUR_DOMAIN] and [YOUR_CERTIFICATE] with your own information wherever they appear in the code above.

The first server block redirects our domain's traffic from HTTP (port 80) to HTTPS (port 443). The second block listens on port 443 and uses the SSL certificate and key as well as the SSL configuration. The location block then sets the localhost location and establishes necessary headers and settings. When you're finished, save this file.

We're almost done! Let's start our app's Node webserver. Change directories to your mean-rsvp folder on your VPS. This is likely to be:

$ cd /mean-rsvp

Once you're in your project folder, start the webserver with PM2 with the following command:

$ sudo pm2 start server.js

The final step is to start the nginx reverse proxy:

$ sudo systemctl start nginx

You should now be able to access your site in the browser. It should redirect to a secure connection if https is not specified when entering the URL.

Note: We haven't updated our Auth0 Client settings yet, so we'll get an error if we try to log in. We'll fix that next!


Production Auth0 Settings

There's only one step left, and that's to update our app's Auth0 Client settings to accommodate the production environment.

Log into Auth0 and head to your Auth0 Dashboard Clients. Select your RSVP app Client and add the production URLs to your settings:

  • Allowed Callback URLs - https://[YOUR_DOMAIN]/callback, https://[YOUR_DOMAIN]/silent
  • Allowed Origins (CORS) - https://[YOUR_DOMAIN]

Note: Take note that these are secure URLs using HTTPS.

If you have social connections set up for login, make sure they aren't using Auth0 dev keys. There will be an orange ! icon next to any that are. If so, you'll need to provide your own App or Client ID instead of leaving the field blank. Each connection has instructions on how to obtain your own ID.

After saving your changes in the Auth0 dashboard, you should be able to log into your production application!


Conclusion

We've now built and deployed a MEAN stack single page app to production, complete with:

  • API with authorized CRUD operations
  • NoSQL database
  • Authentication and access management with automatic JWT renewal
  • Simple and complex forms with custom validation
  • Lazy loading
  • SSL

Homework

There were a few topics that I didn't cover in this tutorial series in the interest of keeping it more manageable. If you'd like to dig deeper, I highly recommend that you look into the following:

Testing

Self-hosted MongoDB

Congratulations!

You've completed a real-world, full-stack JavaScript project and even had a little taste of operations / sysadmin. You should now be prepared to build and deploy your own MEAN stack applications from the ground up!