Setting Up Node.js CI/CD on EC2 with GitHub Actions and Nginx

Search for a command to run...

Everything looks good, but could you also configure or set up MongoDB in GitHub Actions while deploying to the EC2 instance?
While working on one of the feature in Leadlly, a platform designed to help students organize and maintain their self-studies, I encountered an interesting issue that got me thinking about race conditions in code. The app is designed to generate new ...

1. What is a Payment Gateway? A payment gateway is a service that facilitates online payments between buyers and sellers. It acts as a bridge between the customer, the merchant, and the bank. Payment gateways securely transmit payment information for...

JSON Web Token (JWT) is widely used for handling authentication and authorization in modern web applications. JWT allows secure and compact transmission of information between parties as a JSON object, and this information is verifiable and trusted b...

We know, as applications grow in complexity and usage, efficient, secure, and scalable data handling becomes crucial. Handling file uploads is a common requirement in web applications, especially in scenarios where users upload large amounts of data ...

Scaling backend infrastructure is one of the most critical steps in handling growing traffic and ensuring smooth operation under heavy loads. As systems grow, a single server processing all the tasks synchronously can become a bottleneck. This is whe...

Continuous Integration and Continuous Deployment (CI/CD) helps automate the process of testing and deploying code to production. In this blog, we’ll walk through setting up a CI/CD pipeline for a Node.js application using GitHub Actions and deploying it to an AWS EC2 instance.
Before diving into the setup, ensure you have the following:
AWS Account: You will need access to AWS with permission to create and manage EC2 instances.
Node.js Application: We will assume you have a Node.js project already set up.
GitHub Repository: The Node.js application should be in a GitHub repository.
To begin, we’ll launch an EC2 instance that will serve as the deployment target for our Node.js application.
Log in to your AWS Console and go to the EC2 Dashboard.
Click on Launch Instance

Choose an Amazon Machine Image (AMI). Select Ubuntu or Amazon Linux for Node.js applications.

Choose the Instance Type. For a basic application, the free-tier eligible t2.micro is sufficient and setup a keypair
Key Pair: Create a new key pair or use an existing one. Make sure to download the private key (.pem file), as you will need it to connect to the instance later.

Configure Instance Details. In most cases, the default settings are fine.
Add Storage. Use the default storage allocation (8 GB is typically enough for a small Node.js app).
Configure Security Group:
Create a new security group and add the following rules:
SSH (port 22): Allow your IP address to connect via SSH.
HTTP (port 80): Allow access to your application.
You can also add HTTPS (port 443) if you plan to use SSL.

Launch the Instance.

After the instance is launched, make a note of its Public IP address or Public DNS.

Once the EC2 instance is running, connect to it using SSH and set up Node.js.
Connect to the EC2 Instance: Open a terminal and run the following command to SSH into your instance:
ssh -i /path/to/your-key.pem ubuntu@your-ec2-public-ip

Update the Package List:
sudo apt update
Install Node.js: You can install the latest stable version of Node.js
sudo apt-get install -y nodejs
This installs the latest LTS (Long-Term Support) version of Node.js.
Install Nginx: We’ll install Nginx, which we’ll use as a reverse proxy for our Node.js application.
Install Nginx:
sudo apt-get install nginx
Start Nginx and enable it to run on startup:
sudo systemctl start nginx
sudo systemctl enable nginx
Check the status of Nginx to ensure it is running:
sudo systemctl status nginx
At this point, Nginx should be running and accessible via the public IP of your EC2 instance. Open a browser and navigate to http://your-ec2-public-ip, and you should see the default Nginx welcome page.
Install PM2: PM2 is a process manager for Node.js applications. It ensures that your application runs in the background and automatically restarts if it crashes.
sudo npm install -g pm2
Now, we’ll set up GitHub Actions to automatically deploy your Node.js application to the EC2 instance whenever you push changes to the main branch.
Create a Self-Hosted Runner in GitHub:
Go to your GitHub repository, click on Settings > Actions > Runners, and click on New self-hosted runner button.

Select the OS type for your runner (Linux for an Ubuntu EC2 instance).

GitHub will show a set of instructions and a registration token, which you’ll use to set up the runner on your EC2 instance. Run those commands one by one on your EC2 instance.
Once you’ve completed the runner registration steps, you can install the runner as a systemd service.
Install the service by running the following command in your runner directory:
sudo ./svc.sh install
This command sets up the runner as a system service, so you don’t need to manually start it every time the EC2 instance is restarted or after logging out.
Start the Runner Service
sudo ./svc.sh start
This will start the self-hosted runner as a background service, and it will be ready to accept and execute workflows from your GitHub repository.
Check the Service Status
sudo ./svc.sh status
This command will show you whether the runner service is active. The output should display something like:
● actions.runner.your-repo.service - GitHub Actions Runner (your-repo)
Loaded: loaded (/etc/systemd/system/actions.runner.your-repo.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2024-10-10 14:00:00 UTC; 5min ago
...
Create the GitHub Actions Workflow: Inside your Node.js project, create a directory .github/workflows/ and add a file named deploy.yml.
name: Node.js CI/CD
# Trigger the workflow on pushes to the "main" branch
on:
push:
branches: [ "main" ]
jobs:
build:
# This job runs on a self-hosted runner, in this case, your EC2 instance
runs-on: self-hosted
steps:
# Step 1: Checkout the latest code from the GitHub repository
- uses: actions/checkout@v4
# Step 2: Setup Node.js environment using the specified version (e.g., 20.x)
- name: Use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: '20.x' # Specify Node.js version
cache: 'npm'
# Step 3: Install dependencies
- run: npm ci
# Step 4: Set up the environment variables by creating a .env file
# Secrets stored in GitHub will be used in the production environment
- run: |
touch .env
echo "${{ secrets.PROD_ENV }}" > .env
# Step 5: Build the project (only if a build script is present in package.json)
- run: npm run build --if-present
# Step 6: This step you have to update after setting up the nodejs application on ec2 instance
# Restart the Node.js app using PM2 for zero-downtime deployment
# - run: pm2 restart node-app
Workflow Overview
Trigger: The workflow is triggered every time code is pushed to the main branch of your GitHub repository.
Runner: The job is executed on a self-hosted runner, which is typically an EC2 instance or any server you control. This is where the application will be deployed.
Verify Workflow Execution: After pushing your changes to the main branch and running the GitHub Actions workflow, your code should be deployed to the EC2 instance. GitHub Actions will place the code inside the _work folder in your runner directory.
To access this folder:
SSH into your EC2 instance:
ssh -i /path/to/your/key.pem ubuntu@your-ec2-ip
Navigate to the GitHub Actions runner directory:
By default, the runner's working directory will be something like:
cd ~/actions-runner/_work/<your-repo-name>/<your-repo-name>
In this directory, you'll find the checked-out code from your repository after the GitHub Actions workflow runs.
Setup Nginx as a Reverse Proxy: Next, you need to set up Nginx to act as a reverse proxy for your Node.js application. This ensures that requests to your EC2 instance (e.g., through its public IP or domain) are routed to your Node.js app.
Configure Nginx for your Node.js app:
Open the Nginx configuration file for editing:
sudo vim /etc/nginx/sites-available/default
Replace the contents with the following Nginx configuration, adjusting it for your app’s port (usually 3000 or another port if configured):
server {
listen 80;
server_name _;
# Add this part to your file
location /api {
rewrite ^\/api\/(.*)$ /api/$1 break;
proxy_pass http://localhost:4000; #assuming your nodejs appliction running on port 4000
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
The configuration specifies that any request path starting with /api will be forwarded to a backend Node.js application running on localhost:4000
Test the Nginx configuration:
Test that your Nginx configuration is valid:
sudo nginx -t
If everything is correct, you should see a message like syntax is ok.
Restart Nginx to apply the changes:
sudo systemctl restart nginx
Start the Node.js App with PM2: To ensure your Node.js application is running, you’ll use PM2 to manage the process.
Start the Node.js application using PM2:
In the same directory where your application is located (inside _work/<your-repo-name>), start your app with PM2:
pm2 start server.js --name node-app # Adjust `server.js` to your app's entry file
This command will run the app in the background and associate it with the name node-app.
Check the status of the app:
Use the following command to ensure that your app is running:
pm2 status
You should see your application listed with the status online.
Enable PM2 startup to ensure it starts on boot:
Run the following command to configure PM2 to start your app automatically after a system reboot:
pm2 startup
Follow the on-screen instructions to complete the setup.
Save the PM2 process list:
To save the current PM2 process list and restore it on reboot, use:
pm2 save
Verify the Setup: Now that Nginx is configured as a reverse proxy and your Node.js app is running under PM2:
Visit your server's public IP or domain name in the browser:
http://your_domain_or_public_ip/api
You should see your Node.js application responding through Nginx.
If everything is set up correctly, your app should now be accessible publicly, with Nginx forwarding incoming requests to your Node.js application.
Now update your github workflow file step 6.
- run: pm2 restart node-app
Now, whenever you push changes to the main branch, GitHub Actions will:
Run the build and tests.
If successful, deploy the updated code to your EC2 instance.
Try making a small change in your repository and pushing it:
git add .
git commit -m "Test CI/CD deployment"
git push origin main
You should see the GitHub Actions workflow running in the Actions tab of your repository. After it completes, the updated code will be deployed to your EC2 instance.
Finally, check that your Node.js application is running on the EC2 instance:
Access the public IP or DNS of your EC2 instance in a web browser:
http://your-ec2-public-ip/api
You should see your Node.js application running!
You’ve now set up a CI/CD pipeline for your Node.js application using GitHub Actions and deployed it to an AWS EC2 instance. This automated process ensures that your code is tested and deployed seamlessly with every push to the main branch.
Feel free to expand this workflow by adding additional stages like database migrations, performance monitoring, or scaling your EC2 instances.