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

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**.

## Prerequisites

Before diving into the setup, ensure you have the following:

1. **AWS Account**: You will need access to AWS with permission to create and manage EC2 instances.
    
2. **Node.js Application**: We will assume you have a Node.js project already set up.
    
3. **GitHub Repository**: The Node.js application should be in a GitHub repository.
    

## Step 1: Launch an EC2 Instance

To begin, we’ll launch an EC2 instance that will serve as the deployment target for our Node.js application.

1. **Log in to your AWS Console** and go to the **EC2 Dashboard**.
    
2. Click on **Launch Instance**
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1728569783817/35d47033-6cb3-490d-9086-fbd9c603f550.png align="center")
    
3. Choose an **Amazon Machine Image (AMI)**. Select **Ubuntu** or **Amazon Linux** for Node.js applications.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1728569843192/522d932e-140f-4b3b-89ff-8c5478231a8a.png align="center")
    
4. Choose the **Instance Type**. For a basic application, the free-tier eligible **t2.micro** is sufficient and setup a keypair
    
5. **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.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1728569907522/d266ce5e-41a8-4d15-84f5-a0ebcdd079ca.png align="center")
    
6. **Configure Instance Details**. In most cases, the default settings are fine.
    
7. **Add Storage**. Use the default storage allocation (8 GB is typically enough for a small Node.js app).
    
8. **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.
        
9. ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1728570118482/7e436041-5e1d-4306-8327-b9996133709a.png align="center")
    
    **Launch the Instance**.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1728570081375/cb15e1e7-d2b9-48fb-8890-a0dee7dbb09a.png align="center")

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1728570211073/972b789c-f7b3-491a-8d66-39e831a3c435.png align="center")

## Step 2: Install Node.js and Set Up the Environment on EC2

Once the EC2 instance is running, connect to it using SSH and set up Node.js.

1. **Connect to the EC2 Instance**: Open a terminal and run the following command to SSH into your instance:
    
    ```bash
    ssh -i /path/to/your-key.pem ubuntu@your-ec2-public-ip
    ```
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1728575667514/f758ce4f-340a-4af0-8129-0df52b705e7a.png align="center")
    
2. **Update the Package List**:
    
    ```bash
    sudo apt update
    ```
    
3. **Install Node.js**: You can install the latest stable version of Node.js
    
    ```bash
    sudo apt-get install -y nodejs
    ```
    
    This installs the latest LTS (Long-Term Support) version of Node.js.
    
4. **Install Nginx:** We’ll install **Nginx**, which we’ll use as a reverse proxy for our Node.js application.
    
    * Install Nginx:
        
        ```bash
        sudo apt-get install nginx
        ```
        
    * Start Nginx and enable it to run on startup:
        
        ```bash
        sudo systemctl start nginx
        sudo systemctl enable nginx
        ```
        
    * Check the status of Nginx to ensure it is running:
        
        ```bash
        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`](http://your-ec2-public-ip), and you should see the default Nginx welcome page.
    
5. **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.
    
    ```bash
    sudo npm install -g pm2
    ```
    

## Step 3: Set Up GitHub Actions for CI/CD

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.

1. **Create a Self-Hosted Runner in GitHub:**
    
    * Go to your GitHub repository, click on **Settings** &gt; **Actions** &gt; **Runners**, and click on **New self-hosted runner** button.
        
    * ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1728589246094/d9dc768c-7209-4998-be19-a1912831ef49.png align="center")
        
        Select the OS type for your runner (Linux for an Ubuntu EC2 instance).
        
    * ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1728590834989/40a66242-4e69-4bc7-b99b-5c459c37c154.png align="center")
        
        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:
            
            ```bash
            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**
            
            ```bash
            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**
            
            ```bash
            sudo ./svc.sh status
            ```
            
            This command will show you whether the runner service is active. The output should display something like:
            
            ```bash
            ● 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
               ...
            ```
            
2. **Create the GitHub Actions Workflow**: Inside your Node.js project, create a directory `.github/workflows/` and add a file named `deploy.yml`.
    
    ```yaml
    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.
        
3. **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**:
        
        ```bash
        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:
        
        ```bash
        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.
        
4. **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:
        
        ```bash
        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):
        
        ```bash
        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`](http://localhost:4000)
        
    * **Test the Nginx configuration**:
        
        Test that your Nginx configuration is valid:
        
        ```bash
        sudo nginx -t
        ```
        
        If everything is correct, you should see a message like `syntax is ok`.
        
    * **Restart Nginx** to apply the changes:
        
        ```bash
        sudo systemctl restart nginx
        ```
        
5. **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:
        
        ```bash
        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:
        
        ```bash
        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:
        
        ```bash
        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:
        
        ```bash
        pm2 save
        ```
        
6. **Verify the Setup:** Now that Nginx is configured as a reverse proxy and your Node.js app is running under PM2:
    
    1. **Visit your server's public IP or domain name** in the browser:
        
        ```plaintext
        http://your_domain_or_public_ip/api
        ```
        
        You should see your Node.js application responding through Nginx.
        
    2. If everything is set up correctly, your app should now be accessible publicly, with Nginx forwarding incoming requests to your Node.js application.
        
    3. Now update your github workflow file step 6.
        
        ```yaml
         - run: pm2 restart node-app
        ```
        

## Step 4: Push Changes to GitHub

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:

```bash
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.

---

## Step 5: Verify the Application

Finally, check that your Node.js application is running on the EC2 instance:

1. Access the public IP or DNS of your EC2 instance in a web browser:
    
    ```bash
    http://your-ec2-public-ip/api
    ```
    
2. You should see your Node.js application running!
    

---

## Conclusion

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.
