[React + Node.js]: Deploy Your MERN Stack App to Amazon EC2 with SSL Encryption

deploy

03/18/2020


Heroku vs Amazon Web Services (AWS)

Both are popular choices for cloud services where you can deploy your web applications and are great for hosting them. Heroku, in my opinion, is a lot simpler when it comes to deploy MERN stack application, but I still wanted to cover them both.

Prerequisites

If you don't have MERN Stack project available, here is how you can set up a basic MERN stack application with REST API.

- [React + Node.js]: Create your MERN Stack Application - Part 1: Backend

- [React + Node.js]: Create your MERN Stack Application - Part 2: Frontend

In addition, be sure to go through some steps from the last post on how to..

  1. Serve the static index.html
  2. Build your client
  3. Add whitelist IP on MongoDB

You can choose skip above steps and download the project. I'll be using this project to deploy on AWS EC2.

1. Create Ubuntu AWS EC2 Instance

Go to AWS console to sign up with your account if you haven't. Once singed in, Launch a virtual machine

1

Search for ubuntu and select Ubuntu Server 18.04 LTS (HVM), SSD Volume Type 2

Make sure that t2.micro (Free tier eligible) is checked and click 6. Configure Security Group. 3

Add more types: HTTP and HTTPS by clicking Add Rule. Once added, click Review and Launch 4

Click Launch 5

Create a new key pair. Name your key (filename), Download Key Pair then click Launch Instances 6

Click View Instances 7

Under Description tab, copy your Public DNS (IPv4) as you'll need it soon

8

2. Connect to your Instance

Open your git bash(windows) or terminal(mac) then browse over to the directory you saved your key file. Then run the command to change file access (User can read only).

BASH
chmod 400 ~/Downloads/aws-key-1022.pem

Then connect to your instance with your copied Public DNS (IPv4). i.e. ssh -i ~/Downloads/aws-key-1022.pem [email protected]

BASH
ssh -i ~/Downloads/aws-key-1022.pem ubuntu@<YOUR_COPIED_PUBLIC_DNS>

3. Install required packages on Ubuntu

Run the below command to install all packages needed to run the server on Ubuntu.

BASH
curl https://ellismin.com/sh/linux-mern-setup | sudo bash

What's inside: linux-mern-setup

BASH
# Add nodejs 10 personal package from nodesource
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
# Install Node.js & npm
sudo apt-get install -y nodejs
# Import GPK key for MongoDB
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 4B7C549A058F8B6B
# Add MongoDB APT repository at /etc/apt/sources.list.d/mongodb.list
echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list
# Instal MongoDB on Ubuntu
sudo apt update
sudo apt-get install -y mongodb-org
# Start MongoDB
sudo systemctl start mongod
# Start MongoDB automatically on startup
sudo systemctl enable mongod
# Install pm2
sudo npm install -g pm2
# Start pm2 automatically on startup
sudo pm2 startup systemd
# Install nginx
sudo apt-get install -y nginx
# Enable UWF--allow SSH with firewall
sudo ufw allow OpenSSH
# Allow HTTP/HTTPS with firewall
sudo ufw allow 'Nginx Full'
# Enable firewall
sudo ufw --force enable

4. Set up your MERN application with nginx

Clone your repository into your Ubuntu machine. Note that I'm using a project that was set up in this post. Client folder(front-end) is located inside the root directory that contains other the back-end code.

BASH
sudo git clone https://github.com/EllisMin/mern-demo-production.git /opt/mern-demo

Tired of github authentication for pulling repository? Create ssh-keygen with cd ~/.ssh && ssh-keygen then keep pressing enter for prompts. Run cat id_rsa.pub to copy the key & paste into github repository --> Settings --> Deploy keys --> Add deploy key.

Use cd ~/ && git clone <Repo's SSH> to clone your project inside root folder. Don't include sudo keyword, and make sure to use SSH key (i.e. [email protected]:EllisMin/repo-name.git), not HTTPS.

Navigate into project folder and install npm packages

BASH
cd mern-demo && sudo npm i

If npm installation fails, you may have to set a flag. sudo npm i --unsafe-perm. Read more

Start the server with pm2

BASH
sudo pm2 start app.js

Why use pm2? While running the server with npm start works, the server may crash if there's an error or unexpected behavior occur to the server. pm2 allows to keep application alive forever by reloading Node.js application without downtime.

Tip: more commands for pm2

BASH
# You can use [filename] instead of all
sudo pm2 stop all # Stops all process running with pm2
sudo pm2 restart all # Restart all process running
sudo pm2 delete all # Remove all process from pm2 list
sudo pm2 logs # Displays log messages
sudo pm2 status # Shows status of running servers

Configuring nginx

Start off by removing the default setting for nginx

BASH
sudo rm /etc/nginx/sites-available/default

Then, create a new default for configuration

BASH
sudo vim /etc/nginx/sites-available/default
# OR
sudo nano /etc/nginx/sites-available/default

/etc/nginx/sites-available/default

NGINX
server {
listen 80;
server_name _;
# Use below this if you need custom domain
# server_name yourDomain.com www.yourDomain.com;
location / {
# Connect node api
proxy_pass http://127.0.0.1:8080; # Use port number accordingly
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
# Setting maximum size of http request from client (OPTIONAL. Default is 1M if not set)
# client_max_body_size 10M;
}
}

To set up your custom domain, make sure to include server_name yourDomain.com www.yourDomain.com; instead of server_name _;

Making changes to this file requires restarting nginx. Restart it.

BASH
sudo systemctl restart nginx
# OR
sudo service nginx restart

Tip: more commands for nginx

BASH
sudo nginx -t # check nginx syntax
sudo systemctl start nginx # start nginx
sudo systemctl stop nginx # stop nginx
sudo systemctl restart nginx # restart nginx
sudo systemctl status nginx # view server status
sudo tail -30 /var/log/nginx/error.log # display error log for debugging

At this point, you should be able to see your app running on server. Navigate to your Public DNS(IPv4) i.e. ec1-4-56-119-211.us-east-2.compute.amazonaws.com. Alternative is to go to IPv4 Public IP i.e. 3.22.154.213.

5. Set up your custom domain

To set up a custom domain, make sure that you've included your domains in nginx config file from the last step.

First, you need to create an Elastic IP address, static IPv4 address which is reachable from the internet. Although you already have IPv4 Public IP, this is subject to change as your instance gets rebooted unexpectedly, which will require you to keep changing A (Alias) record on your domain provider.

In general, using Elastic IP is free as long as it constantly gets used by an instance. In other words, there is no charge unless you reserve and do not use. So, make sure that you don't create a duplicate Elastic IP. Learn more.

Creating Elastic IP Address

On the left panel of EC2 dashboard, click Elastic IPs under NETWORK & SECURITY and click Allocate Elastic IP address  2  3

Click Allocate  4 Click created IP address (i.e. 3.132.123.28). Then click Associate Elastic IP address  5  6

Select your Instance and hit Associate  7

Your Elastic IP is created and associated with your instance.

IMPORTANT: After this step, your `Public DNS(IPv4)` and `IPv4 Public IP` will reset to a new value. You must use the new public DNS to connect to your instance on ssh.

Add records to your domain provider

Grab new Public DNS and Elastic IP and navigate to your domain provider's website (i.e namecheap, GoDaddy, Bluehost, etc). On the DNS setting page, add two following records.

 8  1

Changing records may take some time (typically a few minutes) depending on the domain registrar. Make sure that your website is accessible (both www and naked domain) before proceeding to the next step.

6. Set up your SSL certificate (HTTPS)

Install certbot

BASH
# Install certbot
cd ~ && sudo add-apt-repository ppa:certbot/certbot # Then press enter for prompt
sudo apt-get update && sudo apt-get install python-certbot-nginx # Then press Y

Run certbot after installation

BASH
sudo certbot --nginx -d yourDomain.com -d www.yourDomain.com

It'll prompt you with..

  • Enter your email
  • Press "A" to agree
  • Y/N whether to share your email
  • Press 2 to redirect traffic

Certbot will automatically insert codes inside nginx configuration file:

NGINX
server {
server_name yourDomain.com www.yourDomain.com;
location / {
# Connect node api
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/yourDomain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/yourDomain.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.yourDomain.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = yourDomain.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name yourDomain.com www.yourDomain.com;
return 404; # managed by Certbot
}

This is it! Save default and run sudo systemctl restart nginx to apply change

Create a scheduler to automatically renew your certificate

BASH
sudo select-editor # Choose your favorite editor
sudo crontab -e

Insert this & save

BASH
11 23 * * * /usr/bin/certbot renew --quiet

Your Ubuntu machine will run /usr/bin/certbot renew --quiet every day at 11:11PM for auto renewal of SSL certificate. Read this post to learn more about cron jobs.

Enabling maintenance mode for your app with Nginx

maintenance.html example

maintenance

HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>YOUR_PAGE | Maintenance</title>
<style>
body {
background: #222;
}
.main-container-maintenance {
position: absolute;
top: 40%;
left: 50%;
font-family: Arial, Helvetica, sans-serif;
color: #fada26;
text-align: center;
transform: translate(-50%, -40%);
}
</style>
</head>
<body>
<div class="main-container-maintenance">
<h1>Offline for maintenance</h1>
<p>
This website is undergoing maintenance right now (·_·)
</p>
<p>Please check back later!</p>
</div>
</body>
</html>

Turning On/Off

When the file exists, your app will only serve maintenance.html. Make sure to put it under directory, /opt/maintenance.html

To turn it on or off

BASH
# Turn ON
cd /opt && sudo mv maintenance-.html maintenance.html
# Turn OFF
cd /opt && sudo mv maintenance.html maintenance-.html

It may be convenient to create a shell script. For example, create sudo vim /opt/on-maintenance.sh that contains above command. Make it executable with sudo chmod +x on-maintenance.sh. Then run the script with sh /opt/on-maintenance.sh

nginx config

Modify nginx config to detect whether there exists /opt/maintenance.html.

BASH
sudo vim /etc/nginx/sites-available/default
NGINX
server {
server_name yourDomain.com www.yourDomain.com;
location / {
if (-f /opt/maintenance.html) {
return 503;
}
# Connect node api
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/yourDomain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/yourDomain.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
# When /opt/maintenance.html is found
error_page 503 @maintenance;
location @maintenance {
root /opt;
rewrite ^(.*)$ /maintenance.html break;
}
}
server {
if ($host = www.yourDomain.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = yourDomain.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name yourDomain.com www.yourDomain.com;
return 404; # managed by Certbot
}

Extra: Make deployment easier with alias & shell script

Deploying MERN stack application to EC2 instance requires many steps, but future deployments of the websites can be less painful if you utilize alias on your terminal along with shell script on your EC2 instance.

When making changes to your app, you would need to go through the following steps:

  • Build your client
  • Commit & push changes to GitHub repository
  • Clone it into your EC2
  • Install packages
  • Restart pm2

You can alternatively pull it from repository and re-build on EC2 instance; however, I prefer building it on my own PC beforehand because it's faster.

First, create an alias on your terminal to take care of building your client and pushing changes to the repository.

You can read more about alias in this post

Example: Creating alias to build & push to repo

~/.bash_profile
BASH
# Make sure that you're script is defined to build React code i.e. "npm run build --prefix front-end"
function buildpush() {
npm run build
git add .
git commit -a -m "[PRODUCTION]"
git push
}

Now, running buildpush in your terminal takes care of the first two steps: Build your client, Commit & push changes to GitHub repository

Proceed to your EC2 instance and create the following shell script for the rest of the steps

BASH
sudo vim /opt/production.sh
/opt/production.sh
SHELL
# Remove this folder if exists
sudo rm -rf ~/production-prepare
# Make sure that you've created GitHub SSH authentication on EC2
# This clones into home directory because cloning directly into /opt/ folder requires
# typing GitHub username & password
git clone [email protected]:GITHUB_USERNAME/REPO_NAME.git ~/production-prepare
sudo rm -rf /opt/production-prepare
sudo mv ~/production-prepare /opt/
cd /opt/production-prepare && sudo npm i
sudo pm2 stop all
# Replace previous application
cd /opt && sudo rm -rf mern-demo && sudo mv production-prepare mern-demo && sudo pm2 start mern-demo/app.js

After creating file, make it executable with sudo chmod +x <FILE_NAME>. Then run the script with sh <FILE_NAME>

More Notes

  • To check remaining space in storage of EC2 instance, run df -h

WRITTEN BY

Keeping a record