[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..
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
Search for ubuntu
and select Ubuntu Server 18.04 LTS (HVM), SSD Volume Type
Make sure that t2.micro
(Free tier eligible) is checked and click 6. Configure Security Group
.
Add more types: HTTP
and HTTPS
by clicking Add Rule
. Once added, click Review and Launch
Click Launch
Create a new key pair. Name your key (filename), Download Key Pair
then click Launch Instances
Click View Instances
Under Description
tab, copy your Public DNS (IPv4)
as you'll need it soon
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).
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]
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.
curl https://ellismin.com/sh/linux-mern-setup | sudo bash
What's inside: linux-mern-setup
# Add nodejs 10 personal package from nodesourcecurl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -# Install Node.js & npmsudo apt-get install -y nodejs# Import GPK key for MongoDBsudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 4B7C549A058F8B6B# Add MongoDB APT repository at /etc/apt/sources.list.d/mongodb.listecho "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 Ubuntusudo apt updatesudo apt-get install -y mongodb-org# Start MongoDBsudo systemctl start mongod# Start MongoDB automatically on startupsudo systemctl enable mongod# Install pm2sudo npm install -g pm2# Start pm2 automatically on startupsudo pm2 startup systemd# Install nginxsudo apt-get install -y nginx# Enable UWF--allow SSH with firewallsudo ufw allow OpenSSH# Allow HTTP/HTTPS with firewallsudo ufw allow 'Nginx Full'# Enable firewallsudo 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.
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. Runcat 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 insideroot
folder. Don't includesudo
keyword, and make sure to useSSH
key (i.e. [email protected]:EllisMin/repo-name.git), notHTTPS
.
Navigate into project folder and install npm packages
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
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
# You can use [filename] instead of allsudo pm2 stop all # Stops all process running with pm2sudo pm2 restart all # Restart all process runningsudo pm2 delete all # Remove all process from pm2 listsudo pm2 logs # Displays log messagessudo pm2 status # Shows status of running servers
Configuring nginx
Start off by removing the default setting for nginx
sudo rm /etc/nginx/sites-available/default
Then, create a new default for configuration
sudo vim /etc/nginx/sites-available/default# ORsudo nano /etc/nginx/sites-available/default
/etc/nginx/sites-available/default
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 ofserver_name _;
Making changes to this file requires restarting nginx. Restart it.
sudo systemctl restart nginx# ORsudo service nginx restart
Tip: more commands for nginx
sudo nginx -t # check nginx syntaxsudo systemctl start nginx # start nginxsudo systemctl stop nginx # stop nginxsudo systemctl restart nginx # restart nginxsudo systemctl status nginx # view server statussudo 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
Click Allocate
Click created IP address (i.e. 3.132.123.28). Then click Associate Elastic IP address
Select your Instance and hit Associate
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.
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
# Install certbotcd ~ && sudo add-apt-repository ppa:certbot/certbot # Then press enter for promptsudo apt-get update && sudo apt-get install python-certbot-nginx # Then press Y
Run certbot after installation
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:
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
sudo select-editor # Choose your favorite editorsudo crontab -e
Insert this & save
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
<!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
# Turn ONcd /opt && sudo mv maintenance-.html maintenance.html# Turn OFFcd /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 withsudo chmod +x on-maintenance.sh
. Then run the script withsh /opt/on-maintenance.sh
nginx config
Modify nginx config to detect whether there exists /opt/maintenance.html
.
sudo vim /etc/nginx/sites-available/default
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
# 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
sudo vim /opt/production.sh
# Remove this folder if existssudo 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 & passwordgit 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 applicationcd /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