Automate MERN App Deployment with GitHub Actions CI/CD

Automate MERN App Deployment with GitHub Actions CI/CD

As you continue to develop your software, you must also continue to integrate it with previous code and deploy it to servers. Manually doing this is a time-consuming process that can occasionally result in errors. So we need to do this in a continuous and automated manner, which is what you will learn in this article.

Let's Get Started

The Problem

Consider our productivity app. There are numerous steps we must complete, from building the application to pushing it to the docker hub. First, we must run tests with a command to determine whether all tests are passed or not; if all tests are passed, we build docker images and then push those images to Docker Hub. If your application is extremely complex, you may need to take additional steps. Now, we're doing everything manually, which takes time and can lead to mistakes.

waiting for deployment without devops

The Solution

To address this problem, we can create a CI/CD Pipeline. So, whenever you add a feature or fix a bug, this pipeline is triggered, which automatically performs all of the steps from testing to deploying.

What is CI/CD and Why it's Important?

Continuous Integration and Continuous Deployment is a series of steps performed to automate software integration and deployment. CI/CD is the heart of DevOps.

ci cd steps

From development to deployment, our MERN app goes through four major stages: testing, building docker images, pushing to a registry, and deploying to a cloud provider. All of this is done manually by running various commands. This must be done every time a new feature is added or a bug is fixed. However, this will significantly reduce developer productivity, which is why CI/CD is required to automate this process. (In this article, we will cover till pushing to the registry.)

ci cd meme

What is "GitHub Actions"?

A CI/CD server is required to run a CI/CD pipeline; this is where all of the steps written in a pipeline run. There are numerous services available on the market, including Jenkins, Travis CI, Circle CI, GitLab CI/CD, AWS CodePipeline, Azure DevOps, and Google Cloud Build. I chose GitHub Actions because it is natively integrated with Git, works well with GitHub, and is extremely simple to use. It is also sufficient for small/medium-sized applications such as ours. For more complex applications, you can use any of the other services listed above.

How do GitHub Actions work?

GitHub actions is not a CI/CD platform, CI/CD is one of its use cases.

Events

Workflows are triggered by events such as pull requests, pushed, PR merged, the issue created, and a few others. For example, you can trigger a workflow running a bot that says "Thanks for opening the issue, our maintainers will review it." whenever an issue is created.

Workflows

Workflows are YAML files that contain all of the instructions that are executed after an event is triggered. Workflow files are typically kept in the .github/workflows/ directory.

Jobs

Jobs is a group set of steps called a job. Each workflow must include at least one job. A job is a set of steps that must be executed in a virtual environment.

Runners

This is my favorite feature of GitHub Actions. Runners are virtual environments where jobs are carried out. You can either use GitHub-hosted runners that contains pre-installed packages or host your own.

how gitHub actions work

Adding a CI/CD Pipeline to the MERN App

Prerequisites

  • Get source code from here if haven't followed previous tutorials in this series.

  • Read previous articles in this series so that you won't get confused.

  • Basic YAML understanding.

  • GitHub account.

To begin, we must define pipeline configurations in a YAML file. Where should I save this YAML file? - If you're familiar with GitHub, you've probably heard of the .github directory, which is a special directory used for GitHub-specific items such as issue templates, pull request templates, workflow files, and so on. So we must place our pipeline file in the workflows folder, which is located in the .github folder.

Go ahead and create a YAML file and name it something like pipeline.yml or pipeline.yaml. Your folder structure should now look like this:

.
└── my-project/
    ├── .github/
    │   └── workflows/
    │       └── pipeline.yml
    ├── client/
    ├── server/
    └── docker-compose.yml

First, we have to mention the name of the workflow.

name: Build and Deploy

Then, mention a trigger - on commit/on push/on pull request, etc.., along with branches.

on:
  push:
    branches:
      - main

Since our application uses some environment variables while running tests, we have to set environment variables in the YAML file that will be used by workflow steps.

But, first, let's add environment variables in the GitHub repository. To do so, go to repository Settings -> expand Secrets and variables -> Actions.

path to add secrets in github repo

Click on New repository secret and add the following secrets.

add secrets in github repo

Now in the YAML file, you can access them like this:

env:
  MONGODB_URI: ${{ secrets.MONGODB_URI }}
  TOKEN_KEY: ${{ secrets.TOKEN_KEY }}
  EMAIL: ${{ secrets.EMAIL }}
  PASSWORD: ${{ secrets.PASSWORD }}

Let's get to the interesting part! These are the only steps we have to perform with GitHub Actions. As simple as that.

ci cd pipeline steps

Start by adding jobs, let's add a job called build-and-deploy.

jobs:
  build-and-deploy:
    # This is telling GitHub to run the workflow on the latest version of Ubuntu.
    runs-on: ubuntu-latest

Adding Continuous Integration

In this job, let's add steps to pull code from the repo and run some tests.

steps:
      # Checkout the code from the GitHub repository
      - name: Checkout code
        uses: actions/checkout@v3

      # Install dependencies and run tests for the client application
      - name: Install and Test Client
        working-directory: ./client
        run: |
          npm install
          npm run test

      # Install dependencies, export environment variables to be used by application and run tests for the server application
      - name: Install and Test Server
        working-directory: ./server
        run: |
          npm install
          export MONGODB_URI=$MONGODB_URI
          export TOKEN_KEY=$TOKEN_KEY
          export EMAIL=$EMAIL
          export PASSWORD=$PASSWORD
          npm run test
  • name - You can provide a name for each step.

  • uses - Set which action to use. checkout is a pre-defined action provided by GitHub that checks out a repo

  • working-directory - Set working directory. This is where all the commands mentioned in that step will run.

  • run - Run commands. You can run multiple commands by putting | (pipe). Learn more about YAML in this YouTube tutorial.

Now if you push these changes to GitHub. It will automatically run your workflow. You can see the running workflow in the Actions tab of your GitHub repository.

Click on the commit and then the job to see the running steps.

Continuous Integration

Adding Continuous Deployment

Now it's time to build docker images and push them to the docker hub.

# Build a Docker image for the client application
      - name: Build Client Docker Image
        working-directory: ./client
        # Build image with tag rakeshpotnuru/productivity-app:client
        run: |
          docker build -t rakeshpotnuru/productivity-app:client-${{github.run_number}} .

      # Build a Docker image for the server application
      - name: Build Server Docker Image
        working-directory:
          ./server
          # Build image with tag rakeshpotnuru/productivity-app:server
        run: |
          docker build -t rakeshpotnuru/productivity-app:server-${{github.run_number}} .

      # Login to Docker Hub using credentials from repository secrets
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # Push the Docker images to Docker Hub
      - name: Push Docker Images to Docker Hub
        run: |
          docker push rakeshpotnuru/productivity-app:client-${{github.run_number}}
          docker push rakeshpotnuru/productivity-app:server-${{github.run_number}}
  • Before pushing to the docker hub we need to log into the hub. That's why there's a step before pushing images. With with we can use secrets directly without setting them in the environment.

  • github.run_number - This is because it's a good practice to give a unique identifier for every image on the hub. And this environment variable (run_number) will be generated automatically by GitHub Actions.

Continuous Deployment

images pushed to dockerhub

Here's the complete YAML file.

# The name of the workflow.
name: Build and Deploy

# Run the workflow when code is pushed to the main branch
on:
  push:
    branches:
      - main

# Set environment variables
env:
  MONGODB_URI: ${{ secrets.MONGODB_URI }}
  TOKEN_KEY: ${{ secrets.TOKEN_KEY }}
  EMAIL: ${{ secrets.EMAIL }}
  PASSWORD: ${{ secrets.PASSWORD }}

# This is the workflow that is being run.
jobs:
  build-and-deploy:
    # This is telling GitHub to run the workflow on the latest version of Ubuntu.
    runs-on: ubuntu-latest
    steps:
      # Checkout the code from the GitHub repository
      - name: Checkout code
        uses: actions/checkout@v2

      # Install dependencies and run tests for the client application
      - name: Install and Test Client
        working-directory: ./client
        run: |
          npm install
          npm run test

      # Install dependencies, export environment variables to be used by application and run tests for the server application
      - name: Install and Test Server
        working-directory: ./server
        run: |
          npm install
          export MONGODB_URI=$MONGODB_URI
          export TOKEN_KEY=$TOKEN_KEY
          export EMAIL=$EMAIL
          export PASSWORD=$PASSWORD
          npm run test

      # Build a Docker image for the client application
      - name: Build Client Docker Image
        working-directory: ./client
        # Build image with tag rakeshpotnuru/productivity-app:client
        run: |
          docker build -t rakeshpotnuru/productivity-app:client-${{github.run_number}} .

      # Build a Docker image for the server application
      - name: Build Server Docker Image
        working-directory:
          ./server
          # Build image with tag rakeshpotnuru/productivity-app:server
        run: |
          docker build -t rakeshpotnuru/productivity-app:server-${{github.run_number}} .

      # Login to Docker Hub using credentials from repository secrets
      - name: Login to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # Push the Docker images to Docker Hub
      - name: Push Docker Images to Docker Hub
        run: |
          docker push rakeshpotnuru/productivity-app:client-${{github.run_number}}
          docker push rakeshpotnuru/productivity-app:server-${{github.run_number}}

Resources

Learn more about CI/CD and GitHub Actions.


In the next article, we will learn how to deploy to a cloud service provider (Generally the last step in a CI/CD pipeline).


a world with ci cd meme


Build strong pipelines. Don't be like this 💀 - https://youtu.be/M_jjG9K5KEo


Follow me for more, seriously!

Did you find this article valuable?

Support itsrakesh by becoming a sponsor. Any amount is appreciated!