Practical Guide to Set Up Multiple NodeJS Apps on AWS EC2 Instance with Automatic Deployment using GitHub Actions (Screenshots)

cre8stevedev

Stephen Omoregie

Posted on August 24, 2024

Practical Guide to Set Up Multiple NodeJS Apps on AWS EC2 Instance with Automatic Deployment using GitHub Actions (Screenshots)

Introduction

First of all, this is a verrrrrryyyyy loooooooooonnng epistle. Feel free to skim through to get the gist, and then come back again when you need to use it for a project.

In this guide, I'll be as detailed as possible guiding you step by step on setting up your EC2 Instance, your code repository up to testing your deployment. Bookmark! Let's go!

Setting Up an EC2 Instance on AWS.

Navigate to the EC2 dashboard.
Navigating to the EC2 Dashboard

Click on "Launch Instance" to create a new EC2 instance.
Click Launch Instance on Dashboard

Choose an Amazon Machine Image (AMI) (We'll use an Ubuntu Server AMI for this setup).
Setting Up the Instance 01

Select an instance type based on your resource needs (e.g., t2.micro for testing or t2.small/medium for production).
T2.Micro Free Tier Eligible

Create a key-value pair (use RSA) and store the file in a secure location on your PC. You'll be needing it for SSH Access later.
Create key-value pair for SSH

Select appropriate network settings for your instance.
You can choose to allow access to your instance from anywhere (this is not recommended though - use your IP Address, or add a custom IP address from whence you can ssh into the instance).

  • Allow SSH access (port 22) from your IP address.
  • Allow HTTP (port 80) and HTTPS (port 443) from anywhere. Select appropriate network settings

Set up storage as needed for your project: Free-tier eligible instances can get up to 30GB storage. So let's just stick with 8GB for now.
Project Storage

Advanced details? There's a bunch of stuff under that section. You can read up on that to know if you would want to modify any of the default settings.
Advanced Details Section

Review (Some weird JSON stuff, haha) and then Launch your Instance

Successfully Launched Instance

Click on Connect To Your Instance for the Next Steps
Connect to instance
Take note of your public IP and username (ubuntu), you can use the ssh command to get into your instance via your PC:

ssh -i /path/to/your-key-pair.pem ubuntu@your-instance-public-ip
Enter fullscreen mode Exit fullscreen mode

Or just use the browser-based console by clicking on Connect To Instance
Browser-based EC2 Instance access

Browser-based Console. Yaaaaay! We're in.

Setting Up the Environment

We'll make the most of the web console provided to us on the AWS platform to access our launched instance.

*Update the system packages *

sudo apt update && sudo apt upgrade -y
Enter fullscreen mode Exit fullscreen mode

Install Node.js and Node Package Manager (npm)

curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
Enter fullscreen mode Exit fullscreen mode

*Install Git for Version control - I mean, why not! *

sudo apt install git -y
Enter fullscreen mode Exit fullscreen mode

Install PM2 globally (A process manager for managing multiple Nodejs application on the same machine).

sudo npm install pm2@latest -g
Enter fullscreen mode Exit fullscreen mode

Install and enable Nginix for reverse proxying

sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
Enter fullscreen mode Exit fullscreen mode

Create a directory on the home (~) path where we'll house our different app source code.

mkdir ~/apps
cd ~/apps
Enter fullscreen mode Exit fullscreen mode

Cloud environment set up

The steps above will help us prepare our instance with the necessary software to run the Node.js applications and set up a reverse proxy for routing requests. Now, to the next step

Configuring Nginx as a Reverse Proxy

Remember, web requests are sent over HTTP/HTTPS which connect to your port 80/8080/443. With your node applications running on other non-web-ports, we'll need a web server like Nginix that will "forward" all HTTP requests that hits our server to the appropriate ports where the application we want to interact with is running - this is a better approach than exposing your other ports directly to the internet.

Create a new Nginix Configuration file
Let's create a file where we'll store the configuration for proxying all requests to our applications using the server name (your domain or you can use your instance's Public IP Address in the meantime).

sudo nano /etc/nginx/sites-available/nodejs-apps
Enter fullscreen mode Exit fullscreen mode
server {
    listen 80;

    # This server block will server our apps on the same domain 
    # If you have multiple apps serving multiple domains you can 
    # create a new server block for that domain and the set up is 
    # pretty much the same.

    server_name 00.00.99.40; # Your domain or public IP here

    location /app1 {
        proxy_pass http://localhost:3001/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    location /app2 {
        proxy_pass http://localhost:3002/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
Enter fullscreen mode Exit fullscreen mode

Paste the configuration above and modify accordingly. You can delete the default configuration file in /etc/nginx/sites-enabled/default so that it doesn't take precedence over your configuration or modify it accordingly.

Note: make sure the trailing / after the http://localhost:3001/ remains. This instructs nginx to strip off the /app1 and /app2 for example when forwarding the route to your node server.

Enable the new configuration file

sudo ln -s /etc/nginx/sites-available/nodejs-apps /etc/nginx/sites-enabled/
Enter fullscreen mode Exit fullscreen mode

Test the configuration to ensure the your settings are valid.

sudo nginx -t
sudo systemctl reload nginx
systemctl status nginx

Enter fullscreen mode Exit fullscreen mode

Nginix set up and confirmation

And we're all set. Let's get our code in here.

Set up GitHub repositories and clone your applications

For each of your Node.js applications, create a separate GitHub repository if you haven't already. I have created a very simple NodeJS Application that we'll be deploying for testing purposes.

You can find them here:

  1. https://github.com/Cre8steveDev/Deploying_Multiple_Node_App_001
  2. https://github.com/Cre8steveDev/Deploying_Multiple_Node_App_002

Navigate to the apps directory we created earlier in ~apps

cd ~/apps
Enter fullscreen mode Exit fullscreen mode

Clone each of your repositories (First add your SSH Keys on Github)
Generate an ssh key to set up SSH Authentication for your ec2 instance on Github.

ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
cat ~/.ssh/id_rsa.pub
Enter fullscreen mode Exit fullscreen mode

Just hit enter for the prompts. Copy the code displayed by the cat command, and add it to your SSH Keys on github.

Adding SSH on github

Added SSH key

git clone git@github.com:Cre8steveDev/Deploying_Multiple_Node_App_001.git
git clone git@github.com:Cre8steveDev/Deploying_Multiple_Node_App_002.git
Enter fullscreen mode Exit fullscreen mode

For each application, navigate to its directory and install dependencies

cd Deploying_Multiple_Node_App_001
npm install
cd ../Deploying_Multiple_Node_App_001
npm install
# If you're deploying more apps, do the same for all
Enter fullscreen mode Exit fullscreen mode

Create a .env file for each application if needed, to store environment-specific variables. In our case, we're storing the PORT number in the .env, so we'll do that for each of the cloned directory:

echo "PORT=3001" > .env
# and the appropriate port for the others
Enter fullscreen mode Exit fullscreen mode

Test each application to ensure it runs correctly (If you're following the sample server code provided, you can build the project with):

npm run build # Convert typescript to js 
npm run start # Run the server from the dist directory
Enter fullscreen mode Exit fullscreen mode

Testing the Applications

Set up PM2 to manage your Node.js applications

Create a PM2 ecosystem file to manage all your applications

cd ~/apps
pm2 ecosystem generate nodejs-apps
Enter fullscreen mode Exit fullscreen mode

Open the generated ecosystem.config.js using your favourite editor: nano or vim or emacs - no vscode here haha

Here's an example configuration for serving both our applications. It's quite explanatory.

module.exports = {
  apps: [
    {
      name: "Node_App_001",
      script: "./Deploying_Multiple_Node_App_001/dist/index.js",
      env: {
        NODE_ENV: "production",
        PORT: 3001
      }
    },
    {
      name: "Node_App_002",
      script: "./Deploying_Multiple_Node_App_001/dist/index.js",
      env: {
        NODE_ENV: "production",
        PORT: 3002
      }
    },
  ]
};
Enter fullscreen mode Exit fullscreen mode

Now, start all your applications using PM2 command, on the ~/apps directory

pm2 start ecosystem.config.js
Enter fullscreen mode Exit fullscreen mode

Started Nodejs apps

Set up PM2 to start on system boot

pm2 startup systemd
Enter fullscreen mode Exit fullscreen mode

Follow the instructions provided by the command to complete the setup - basically, copy the bash command, that is displayed, paste and hit enter.

Startup file for pm2

Save the current PM2 process list

pm2 save
Enter fullscreen mode Exit fullscreen mode

This step ensures that your Node.js applications are managed efficiently and will restart automatically if the EC2 instance reboots.

Set up automatic deployment using GitHub Actions

Step 1: In each of your GitHub repositories, create a new directory for GitHub Actions - You can do this via your code editor (Not on your server. Your local machine - where you work on your codebase ):

mkdir -p .github/workflows
Enter fullscreen mode Exit fullscreen mode


bash
Create a deployment workflow file in each repository

nano .github/workflows/deploy.yml
# You can use your editor of course.
Enter fullscreen mode Exit fullscreen mode

Step 3: Add the following content to the deploy.yml file (Make sure to customize for each application/repository - paying special attention to the last 5 lines where you define the name of the directory for each application and the command to run after each pull of new updates from GitHub). The file below is for the App 01. modify accordingly for all the repository you intend to setup GitHub actions on.

name: Deploy to EC2 for Application Server 01

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Deploy to EC2 for Application Server 01
        env:
          PRIVATE_KEY: ${{ secrets.EC2_PRIVATE_KEY }}
          HOST: ${{ secrets.EC2_HOST }}
          USER: ubuntu
        run: |
          echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
          ssh -o StrictHostKeyChecking=no -i private_key ${USER}@${HOST} '
            cd ~/apps/Deploying_Multiple_Node_App_001 &&
            git pull origin main &&
            npm install && npm run build &&
            pm2 stop Node_App_001 || pm2 delete Node_App_001;  pm2 start npm --name "Node_App_001" -- start && pm2 save
          '
Enter fullscreen mode Exit fullscreen mode

pm2 restart Node_App_001 the Node_App_001 is the name of the app we created in the ecosystem.config.js, remember?

The secrets.EC2_PRIVATE_KEY is the key-pair .pem file we created earlier when we launched our instance. Remember, I asked you to keep it secure - If you didn't, you can create a new key-value pair on your instance console and add it on github. DO NOT Paste it directly in the deploy.yml file.

In your GitHub repository settings, go to "Secrets and variables" > "Actions" and add two new secrets:
Add Secrets on The individual Code Repositories on Github

EC2_PRIVATE_KEY: The content of your EC2 instance's private key file (copy and paste everything in the value field)
EC2_HOST: Your EC2 instance's public IP address or domain name

View the actions tab on the individual repositories to confirm
Github Actions

Now push these new configuration to GitHub. This step sets up automatic deployment for your applications whenever you push changes to the main branch of your GitHub repositories. The script within the run of your deploy.yml will restart the appropriate node process of your app and restart it - make sure you update it accordingly for your use case.

Update your codebase with new code, commit and push to the individual repository and check it out on your EC2 Instance to confirm that it pulled the new changes

Check the status of the github action on github
Git hub actions

Github actions 2

Checkout both apps running live (I will delete the instance after this tutorial - but you can see that they're being served based on the url /app1 and /app2).

App Demo 01

App demo 02

Take a break!

Phew! If you've made it this far, wow! If you've not had any errors, wow! wow! Haha, but I've made sure every step is covered to the letter.

Set up SSL/TLS for your domain using Let's Encrypt and Certbot

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d your-domain.com
Enter fullscreen mode Exit fullscreen mode

Unfortunately, I do not have an active/free domain to show screenshots for this step. However, the process is quite easy. Just use your domain, follow the prompt and the ssl certificate will be generated for you (You'll have to renew every 90 days or so - or you can set up a script to help with that).

After generating the ssl certificate, update your nginx configuration , basically telling nginx to redirect every http call to https. This is what it would look like:

server {
    listen 80;
    server_name your_domain.com;  # Replace with your domain or IP
    return 301 https://$server_name$request_uri;  # Redirect HTTP to HTTPS
}

server {
    listen 443 ssl;
    server_name your_domain.com;  # Replace with your domain or IP

    ssl_certificate /path/to/your/fullchain.pem;  # Path to your SSL certificate
    ssl_certificate_key /path/to/your/privkey.pem;  # Path to your SSL private key

    # Additional recommended SSL settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    location /app1 {
        proxy_pass http://localhost:3001/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        #return 200 'Nginx is handling /app1 correctly';
    }

    location /app2 {
        proxy_pass http://localhost:3002/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
Enter fullscreen mode Exit fullscreen mode

Test the configuration to ensure the your settings are valid.

sudo nginx -t
sudo systemctl reload nginx
systemctl status nginx

Enter fullscreen mode Exit fullscreen mode

And that's a wrap!

Conclusion

Thanks for reading through. I hope you find the guide helpful as you explore this adventurous ride in deploying your projects on your own VPS (AWS EC2 instance in this case) - the same idea can be implemented on any self-hosted server too. Cheers, and have fun building!

Additional Resources:

  1. PM2 Documentation - https://www.npmjs.com/package/pm2
  2. Github Actions Workflow - https://docs.github.com/en/actions/writing-workflows
  3. Register for AWS Free Tier Services - https://aws.amazon.com/free/
  4. Nginix Documentation: https://nginx.org/en/docs/beginners_guide.html

Take Note

  • Free Tier Limits: The AWS Free Tier offers 750 hours of t2.micro or t3.micro instances each month for the first 12 months after you sign up. This means you can run one instance continuously for the entire month without incurring charges, or you can run multiple instances as long as the total running hours do not exceed 750 (This is fine for your prototyping/hosting side projects. Hence this guide to maximize your ec2 resource).
  • Instance Types: Only specific instance types (t2.micro and t3.micro) qualify for the Free Tier. If you use a different instance type, you will incur charges.
  • Region Availability: Ensure that the instance types you want to use are available in your selected AWS region, as Free Tier eligibility can vary by region.
💖 💪 🙅 🚩
cre8stevedev
Stephen Omoregie

Posted on August 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related