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

deploy

03/18/2020


This post is stale

Stale because using Route 53 & Elastic Load Balancer & Amazon Certificate Manager are not necessary. i.e. this post tells to use both Certificate Manager and letsencrypt that are duplicate processes. New post is available here

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 in file: 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.

Navigate into project folder and install npm packages

BASH
cd mern-demo && sudo npm i

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 default_server;
server_name _;
# Path to front-end index.html
location / {
root /opt/mern-demo/front-end/build;
try_files $uri /index.html;
}
}

Change the path to your project's build folder of client side.

i.e. /opt/<YOUR_PROJECT_FOLDER>/<CLIENT_FOLDER>/build

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 i.e. route traffic to EC2 Instance

Setting up your custom domain is possible through Route 53 service which is a cheap, paid service (50 cents per month up to 25 hosted zone). Read More about pricing

Go to AWS console and launch Route 53 9

Start DNS Management 10

Create Hosted Zone 11

Type in your domain i.e. yourDomain.com and Create 12

Copy these 4 values, ns-.... then navigate to your domain provider. i.e. namecheap 13

Paste in copied values into Custom DNS name servers on your domain provider's website 14

Changing name server may take some time depending on the domain provider

6. Set up your SSL certificate (HTTPS)

Certificate validation with domain

AWS Certificate Manager provides a free public SSL/TLS certificates. On AWS console. launch Certificate Manager 15

Request a certificate 16

Select Request a public certificate then Request a certificate 17

Choose your domain(s). i.e. yourDomain.com and www.yourDomain.com 18

Choose your validation method. DNS validation would be available through Route 53 or other domain name providers such as Namecheap, GoDaddy, etc. 19

Click Review 20

Confirm and request 21

(Route 53 users): if your already using Route 53 with custom domain pointing to your EC2 instance, simply click Create record in Route 53 button(s). Then you'll be able to view created CNAME records in your hosted zone 24 25

For those who are not using Route 53 and their domain name provider allows to alter DNS record, create these records accordingly 22

After setting those records, it would typically take about 3-10 minutes to be validated

If DNS validation somehow does not work out, try validating with Email address--Amazon will send verification emails to your registered emails with your domain provider. 23

Once you've had your certificate validated with your domain, move onto the next step

Create Load Balancer

Load balancers, in a nutshell, accept users' traffic and reroutes to the right server.

Elastic Load Balancing pricing--this is pretty cheap & your app should have it anyway.

Launch your EC2 instance and click Load balancers from EC2 Dashboard then click Create Load Balancer. 26

Select Application Load balancer for HTTP/HTTPS 27

If you'd like, read more about Load balancer Types

Name your balancer, Add listener for HTTPS, and check all availability zones. Then move to Configure Security Settings 28

This is where you connect the certificate to your balancer. Select the certificate and move to Configure Security Groups 29

Select the security group that you've previously when creating EC2 instance i.e. launch-wizard-1 then move to next Configure Routing 30

Set new target group with a name and select protocol as HTTPS for target group and for health checks. Move to Register Targets 31

Check your running instance, Add to registered and check the added instance on upper panel. Then click Next: Review and Create 32

Select the created balancer and click Listeners tab on the lower panel. Select listener for HTTP: 80 then click Edit 33

Remove the default action 34

Then create the following action and click Update 35

TODO: Missing adding two A(Alias) records to Route53

Modify nginx config & Install certbot

Change content of nginx config

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

Change the server_name to your domain names for rerouting

NGINX
# Reroute to HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name <yourDomain.com> <www.yourDomain.com>;
return 301 https://$server_name$request_uri;
}
server {
client_max_body_size 10m; # Increase upload size limit
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;
}
}
BASH
# Install certbot
cd ~ && sudo add-apt-repository ppa:certbot/certbot # Then press enter
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{10-16,
# Reroute to HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name yourDomain.com www.yourDomain.com;
return 301 https://$server_name$request_uri;
}
server {
server_name yourDomain.com www.yourDomain.com; # managed by Certbot
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
client_max_body_size 10m; # Increase upload size limit
# Check if maintenance is on
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;
}
}
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

Enable maintenance mode for your app

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>Ramen Kid | 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

nginx config

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

BASH
sudo vim /etc/nginx/sites-available/default
NGINX{21-23,
# Reroute to HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name yourDomain.com www.yourDomain.com;
return 301 https://$server_name$request_uri;
}
server {
server_name yourDomain.com www.yourDomain.com; # managed by Certbot
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
# Increase upload size limit
client_max_body_size 10m;
# Check if maintenance is on
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;
}
# 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
}

More Notes

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

WRITTEN BY

Keeping a record