DEV Community

Cover image for Deploy React App & Nestjs App to AWS EC2 Instance
Faithful Olaleru
Faithful Olaleru

Posted on

Deploy React App & Nestjs App to AWS EC2 Instance

TABLE OF CONTENT

⚙️ INTRODUCTION

Deploying modern web applications involves more than just writing code; it requires a streamlined process to ensure that applications are reliably built, tested, and deployed. In this article, we will guide you through the deployment of a React front-end application and a NestJS back-end application to an AWS EC2 instance. By leveraging CI/CD pipelines with GitHub Actions, we will demonstrate how to automate the deployment process, ensuring that your applications are consistently and efficiently delivered to users.

AWS EC2 provides a scalable, customizable environment that can host both your front-end and back-end applications. React, a popular JavaScript library for building user interfaces, and NestJS, a progressive Node.js framework for building efficient and scalable server-side applications, are a powerful combination for modern web development.

We will start by setting up our AWS EC2 instance, configuring the necessary environments, and deploying our applications. We will then dive into setting up GitHub Actions, illustrating how to create a CI/CD pipeline that automates the build and deployment process. This approach not only saves time but also minimizes human error, ensuring a seamless and repeatable deployment strategy.

By the end of this article, you will have a robust deployment workflow for your React and NestJS applications, empowering you to focus more on development and innovation, and less on the intricacies of manual deployments. Whether you're a seasoned developer or just getting started with cloud deployments, this guide will provide you with the knowledge and tools to enhance your deployment practices. Let's dive in!

🧰 WHAT YOU NEED

  • Nodejs/Nestjs project
  • React app project
  • Github account
  • AWS account
  • Basic understanding of javascript, AWS, and git/github

🛠 SETUP EC2

Login to your AWS account and create an EC2 instance. Don't forget to save your keypair .pem file and allow inbound traffic for http and ssh for your security group. Now login to your instance using Instance connect or Ssh Client.

If using ssh client, use the coode block below. The first line is to change permissions for your keypair so it's not public. The next line is to actually connect. The actual command depends on the AMI image your instance uses. Sometimes it's root or ec2-user, then the public ip address of your instance preceded by 'ec2-', and then the region.

chmod 400 keypair.pem
ssh -i "keypair.pem" ubuntu@ec2-12-345-678-9.eu-west-2.compute.amazonaws.com
Enter fullscreen mode Exit fullscreen mode

If you have any challenges at this step, visit the helpful links section below to view step-by-step instructions from the official documentations.

Installing Nginx

We would be using Nginx as our reverse proxy. Reverse proxies are important for many reasons, one of which is it adds a layer of security to protect our web servers. Run the commands below:

sudo apt update
sudo apt install nginx -y
Enter fullscreen mode Exit fullscreen mode
Installing Nodejs
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v
Enter fullscreen mode Exit fullscreen mode

You should get a node version after the last line is run.

Now we can install pm2 and serve globally. We'd need them later:

npm i -g pm2
npm i -g serve
Enter fullscreen mode Exit fullscreen mode

PM2 is a production process manager for Node.js apps, ensuring they run continuously and efficiently. It handles tasks like restarting crashed applications, load balancing in cluster mode, and managing logs. PM2 simplifies monitoring and maintenance, making it easier to keep applications stable and performant in production.

Serve is a simple command-line HTTP server for serving static files. It's easy to use, allowing you to quickly host and view your static site or single-page application. With minimal setup, serve efficiently delivers your content, making it a handy tool for development and testing environments.

🪢 SETUP CI/CD PIPELINE

CI/CD is a very robust subject, but for the purposes of this guide, I'd summarize it. We'd be using Github actions, feel free to read more from their documentation in the link at the end.

CI/CD involves continuous integration, where code changes are automatically tested and merged, and continuous deployment, where these changes are automatically released to production. In GitHub Actions, workflows are defined in YAML files and consist of jobs triggered by events like code pushes. These jobs, which are executed by runners, contain steps that perform tasks such as testing, building, and deploying applications. This setup ensures efficient, reliable, and automated software delivery.

Image description

As shown above, go to the settings tab of your repository, and click on runners on the sub navigation. We'd be using self-hosted runners that we'd run on our EC2 instance. The list of supported OS that support self-hosted runners can be found here

Choose the OS that your ec2 instance uses, and just run the commands. The commands would install github runner on your instance and it would be linked to your github repository. Don't forget to give the runner a unique name, and add 'backend-runner' as a label for the runner.

Also run the code below to start actions runner as a service:

sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status
Enter fullscreen mode Exit fullscreen mode

Now go to Actions, and click the 'New Workflow button'.

Image description

Search with keyword 'node', and select highlighted option. Edit the code you see there and replace with the code below:

name: Node.js CI/CD

on:
  push:
    branches: [ "main" ]
  workflow_dispatch:

jobs:
  build:
    # runs-on: self-hosted
    runs-on: backend-runner
    strategy:
      matrix:
        node-version: [20.x]
    steps:
    - name: 'Cleanup build folder'
      run:  sudo rm -r ${{github.workspace}}/*
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    - run: sudo npm install
    - run: sudo npm run build
    - run: pm2 start pm2.config.js 
Enter fullscreen mode Exit fullscreen mode

Explanation: This workflow would be run when any changes are pushed to the main branch of your repository. Workflow_dispatch means the workflow can also be triggered manually. Ideally, you should run on self-hosted for self hosted runners as we are implementing. But we intend to run two self-hosted runner, one for the react app, and the other for the nestjs app, so we use the runner label to differentiate. We are yet to create our pm2.config.js file, which we will do next.

Duplicate the above steps for the repository with the react app. Make sure the runner names are different, as directories are being created on the ec2 instance for each runner.

The code below would be the workflow for the react app:

name: React.js CI/CD

on:
  push:
    branches: [ "main" ]
  workflow_dispatch:

jobs:
  build:
    # runs-on: self-hosted
    runs-on: frontend-runner
    strategy:
      matrix:
        node-version: [20.x]

    steps:
    - name: 'Cleanup build folder'
      run:  sudo rm -r ${{github.workspace}}/*
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    # - run: sudo yarn install
    - run: sudo npm ci
    - run: vite build
    # - run: npm i -g serve
    - run: sudo chmod 777 /home/ubuntu/frontend-runner/_work/{repo-name}/{repo-name}/node_modules/
    - run: sudo chown -R ubuntu:ubuntu /home/ubuntu/frontend-runner/_work/{repo-name}/{repo-name}/node_modules/
    - run: pm2 start pm2.config.cjs
Enter fullscreen mode Exit fullscreen mode

Explanation: Same explanation here, runs on frontend-runner which is a unique label for the react runner. The two extra run commands are for optional. If you run into permission issues while running the workflow, the first optional command give all users read, write and execute access for node_modules. The second command makes your user to be owner of node_modules. Then we finally run the pm2.config.cjs file.

We can test to see that our two apps are running by running the pm2 list command. The response should look like below:

Image description

Nginx Cofiguration

While still in your instance, enter the command below:

sudo nano /etc/nginx/sites-available/default
Enter fullscreen mode Exit fullscreen mode

Now enter the code below into the editor.

server {

  root /var/www/html;

  # Add index.php to the list if you are using PHP
  index index.html index.htm index.nginx-debian.html;

  listen 80;
  server_name ec2-instance-ip-address;


  location / {

    proxy_set_header        Host $host;
    proxy_set_header        X-Real-IP $remote_addr;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Proto $scheme;

    proxy_pass          http://localhost:5173;
    proxy_read_timeout  90;

    # WebSocket support if using vite bundler
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

  }

  location /api/ {

    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;


    proxy_pass  http://localhost:5000;

    proxy_set_header Host $host;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

  }

}

Enter fullscreen mode Exit fullscreen mode

Then run sudo nginx -t to test your changes, then sudo systemctl restart nginx to restart the nginx server.

Explanation: We have two separate locations in our server, / for the react app, /api/ for nestjs app. Headers are added to api location to handle cors errors. If you bundle your react app with vite, you need to add websockets support. Finally, make sure your proxy_pass is running on the right ports for both app.

🪢 SETUP NESTJS & REACT

In our nestjs package.json, the scripts portion looks like below:

"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"
  },
Enter fullscreen mode Exit fullscreen mode

And finally, our pm2.config.js file

module.exports = {
    apps: [
      {
        name: "backend-app",
        script: "npm",
        args: "run start:prod", 
        autorestart: true,
        watch: false,
        max_memory_restart: "300M",
      },
    ],
  };
Enter fullscreen mode Exit fullscreen mode
React Setup

In our react package.json, the scripts portion looks like below:

"scripts": {
    "dev": "vite --host",
    "prod": "npx serve dist -s -p 5173",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
  },
Enter fullscreen mode Exit fullscreen mode

And our pm2.config.cjs file

module.exports = {
  apps: [
    {
      name: "frontend-app"
      script: "npm",
      args: "run prod", 
      autorestart: true,
      watch: false,
      max_memory_restart: "300M",
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

📋 CONCLUSION

Deploying a React and NestJS application on an AWS EC2 instance with a seamless CI/CD pipeline using GitHub Actions is a powerful demonstration of modern web development practices. By leveraging the scalability and flexibility of AWS, combined with the automation capabilities of GitHub Actions, you can ensure efficient deployment and continuous integration. The use of Nginx to serve both the frontend and backend further streamlines the process, providing a robust solution for handling web traffic and API requests. This setup not only enhances the reliability and performance of your applications but also simplifies the deployment workflow, making it easier to manage updates and scale your infrastructure as needed. Embracing these technologies and best practices paves the way for more efficient, maintainable, and scalable software development.

HAPPY CODING!!!🚀🚀

(Article was written with input from ChatGPT)


🔗 HELPFUL LINKS

Top comments (0)