# Day 77: Github Action

# **🚀 Introduction**

In this blog, we will learn the basics of **GitHub Actions** and understand how it can be used to automate application deployment. We will create a **Node.js application**, containerize it using **Docker**, and set up the project on an **AWS EC2 instance**.

We will also work with **Git and GitHub**, Docker Compose, and create a GitHub Actions workflow that automatically pulls the latest code and deploys the updated application whenever changes are pushed to the `main` branch.

* * *

## **🔸What is Github Actions?**

*   GitHub Actions’ is a tool integrated into GitHub that allows you to **automate your software development workflows** directly from your repository. Whether it’s running tests, deploying applications, or managing releases, ‘GitHub Actions’ provides a flexible way to **optimize the development processes**.
    

## **🔸How ‘GitHub Actions’ works?**

*   ‘GitHub Actions’ is based on the concept of workflows. A workflow is a series of automated steps defined in a YAML file that can be triggered by specific events in the **GitHub repository**. These events can include actions such as: push code or run pull requests.
    

* * *

![](https://cdn.hashnode.com/uploads/covers/64cd282c6e0aaa253b2ac79f/8cd4d859-8a2a-4244-9251-606f77dea6ce.png align="center")

## **🔸**Launching an EC2 Instance

*   I started by launching an **Ubuntu EC2 instance**.
    
*   After connecting to the instance through SSH, I first updated the packages:
    

```plaintext
sudo apt-get update & sudo apt-get upgrade
```

Then I installed npm so that I could create and test the Node.js application.

```plaintext
sudo apt install npm 
```

* * *

## **🔸**Creating a Node.js Application

After installing Node.js, I created a directory for my project:

```plaintext
mkdir nodejs-application-deploy-github-actions
cd nodejs-application-deploy-github-actions
```

I created the Node.js project and its `package.json` file.

The project contains the basic files required for the application, such as:

```plaintext
nodejs-application-deploy-github-actions/
│
├── package.json
├── package-lock.json
├── index.js
└── Dockerfile
```

The `index.js` file contains the Node.js application.

For example, the application runs on port `8080`.

* * *

## **🔸**Creating a Docker File

Next, I created a `Dockerfile` to containerize the Node.js application.

```dockerfile
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 8080

CMD ["node", "index.js"]
```

Let's understand what happens here.

### FROM

```plaintext
FROM node:22-alpine
```

This uses Node.js 22 with Alpine Linux as the base image.

### WORKDIR

```plaintext
WORKDIR /app
```

This creates `/app` as the working directory inside the container.

### COPY

```plaintext
COPY package*.json ./
```

This copies `package.json` and `package-lock.json` into the container.

### RUN

```plaintext
RUN npm install
```

This installs the Node.js dependencies.

### COPY

```plaintext
COPY . .
```

This copies the application source code into the container.

### EXPOSE

```plaintext
EXPOSE 8080
```

The application uses port `8080`.

### CMD

```plaintext
CMD ["node", "index.js"]
```

This starts the Node.js application when the container starts.

* * *

## **🔸**Building the Docker Image

*   Before doing anything with GitHub Actions, I wanted to make sure the Docker image worked correctly.
    

I built the image using:

```plaintext
docker build -t nodejs-app .
```

Docker reads the instructions from the Dockerfile and creates a Docker image.

I could then check the image using:

```plaintext
docker images
```

* * *

## **🔸**Running the Docker Container

After building the image, I ran the container:

```plaintext
sudo docker run -p 8080:8080 nodejs-cicd
```

Here:

*   `-p 8080:8080` maps port `8080` of the host to port `8080` inside the container.
    
*   `nodejs-cicd` gives the container a name.
    

![](https://cdn.hashnode.com/uploads/covers/64cd282c6e0aaa253b2ac79f/01f27587-dce2-48d6-816f-8be1bab7c2a7.png align="center")

I checked whether the container was running:

```plaintext
docker ps
```

Then I tested the application:

```plaintext
curl localhost:8080
```

This confirmed that my Node.js application was successfully running inside the Docker container.

![](https://cdn.hashnode.com/uploads/covers/64cd282c6e0aaa253b2ac79f/f03fe0bc-2596-48b5-8bb1-3041084a2e0c.png align="center")

The flow at this point was:

```plaintext
Node.js Application
        ↓
    Dockerfile
        ↓
   Docker Build
        ↓
   Docker Image
        ↓
 Docker Container
        ↓
    Port 8080
        ↓
 curl localhost:8080
```

* * *

## **🔸**Creating Docker Compose

*   After testing the Docker container manually, I created a `docker-compose.yml` file.
    
*   Docker Compose makes it easier to define and manage containers using a YAML configuration file.
    
*   Instead of remembering a long `docker run` command, I can use:
    

```plaintext
docker compose up -d
```

This becomes especially useful when the application has multiple services.

* * *

## **🔸**Creating a GitHub Repository

*   After setting up and testing the application, I created a GitHub repository for the project.
    
*   Before pushing the project, I created a `.gitignore` file using:
    

```plaintext
npx gitignore node
```

This helps prevent files that should not be committed, such as `node_modules`, from being pushed to GitHub.

Then I initialized Git:

```plaintext
git init
```

I added the files:

```plaintext
git add .
```

Then created my first commit:

```plaintext
git commit -m "Initial project setup"
```

Finally, I connected the local repository with my GitHub repository and pushed the code.

```plaintext
git push -u origin main
```

Now my project was available on GitHub.

* * *

## **🔸**Git Clone and Git Pull

*   While working with Git, I also understood the difference between `git clone` and `git pull`.
    

### git clone

`git clone` is used when we want to get a repository for the first time.

```plaintext
git clone <repository-url>
```

It downloads the repository from GitHub to the local/server environment.

### git pull

`git pull` is used when the repository already exists and we want to get the latest changes.

```plaintext
git pull
```

So the basic difference is:

```plaintext
git clone
    ↓
Get repository for the first time

git pull
    ↓
Update an existing repository
```

* * *

## **🔸**Running the Application Using Docker Compose

After cloning/pulling the repository onto the server, I used:

```plaintext
docker compose up -d
```

This started the application using the configuration from `docker-compose.yml`.

I then tested the application using the server's IP address:

```plaintext
curl http://<IP_ADDRESS>:8080
```

At this point, the application was running on the server.

* * *

## **🔸**Making Changes to the Application

Next, I wanted to simulate a real development workflow.

I made a change to my `index.js` file.

Before getting the latest version, I used:

```plaintext
git pull
```

Then after making the required changes, I committed them:

```plaintext
git add .
git commit -m "Update application"
git push
```

Now the updated code was available in the GitHub repository.

On the server, I could manually update the project using:

```plaintext
git pull
```

Then rebuild and restart the application:

```plaintext
docker compose up -d --build
```

The `--build` option tells Docker Compose to rebuild the image using the updated application code.

This worked, but there was one problem:

> I had to manually connect to the server, run `git pull`, and run Docker Compose every time I made a change.

This is where CI/CD becomes useful.

* * *

## **🔸**Automating Deployment with GitHub Actions

Instead of manually deploying every change, I wanted GitHub to automatically deploy the application whenever I pushed code to the `main` branch.

For GitHub Actions, workflow files are stored inside:

```plaintext
.github/workflows/
```

I created:

```plaintext
.github/workflows/deploy.yml
```

My deployment workflow looks like this:

```yaml
name: Node.js CI

on:
  push:
    branches:
      - main

  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build Docker image
        run: docker build -t nodejs-cicd .
```

* * *

## **🔸**Understanding the GitHub Actions Workflow

Let's understand what happens when I push code.

### Trigger

```plaintext
on:
  push:
    branches:
      - main
```

This tells GitHub Actions to run the workflow whenever I push code to the `main` branch.

* * *

### GitHub Actions Runner

```plaintext
runs-on: ubuntu-latest
```

GitHub provides an Ubuntu runner to execute the workflow.

* * *

### Checkout Code

```plaintext
uses: actions/checkout@v4
```

This checks out the repository code into the GitHub Actions runner.

* * *

### SSH Deployment

The workflow uses:

```plaintext
uses: appleboy/ssh-action@v1.0.3
```

This allows GitHub Actions to connect to my server through SSH.

* * *

## **🔸**What Happens on the Server?

After GitHub Actions connects to the server, these commands are executed:

```plaintext
cd /root/nodejs-application-deploy-github-actions
git pull
docker compose up -d --build
```

The process is:

```plaintext
GitHub Actions
      ↓
     SSH
      ↓
    Server
      ↓
   git pull
      ↓
Updated Code
      ↓
docker compose up -d --build
      ↓
Docker Image Rebuilt
      ↓
New Container Started
      ↓
Updated Application
```

* * *

## **🔸**Testing the Automatic Deployment

Now I made another change in `index.js`.

I committed and pushed the change:

```plaintext
git add .
git commit -m "Update application message"
git push origin main
```

Instead of manually connecting to the server, GitHub Actions automatically started the deployment workflow.

I went to:

```plaintext
GitHub Repository
      ↓
Actions
      ↓
Deploy NodeJS Application
```

I checked the workflow and verified that the steps completed successfully.

Finally, I tested the application again:

```plaintext
curl http://<IP_ADDRESS>:8080
```

The updated response confirmed that the deployment was successful.

* * *

## **🔸**CI vs CD

While working on this project, I also understood the difference between CI and CD.

### Continuous Integration

CI focuses on automatically checking and building the application.

For example:

```plaintext
Code Push
   ↓
Checkout
   ↓
Install Dependencies
   ↓
Test
   ↓
Build
```

### Continuous Deployment

CD focuses on automatically deploying the application.

```plaintext
Build
   ↓
Connect to Server
   ↓
Pull Latest Code
   ↓
Build Docker Image
   ↓
Start Container
   ↓
Application Deployed
```

In my current project, the GitHub Actions workflow mainly handles the **deployment/CD part**.

![](https://cdn.hashnode.com/uploads/covers/64cd282c6e0aaa253b2ac79f/df513cb8-7ae8-4eb4-9744-a393738c8c86.png align="center")

* * *

# **🎯 Conclusion**

In this blog, we learned the basics of **GitHub Actions** and how it can be used to automate the deployment process. We created a Node.js application, containerized it using Docker, and deployed it on an AWS EC2 instance using Docker Compose.

We also worked with **Git and GitHub**, understood `git clone` and `git pull`, and created a GitHub Actions workflow to automatically connect to the server, pull the latest code, rebuild the Docker image, and deploy the updated application.

Finally, we understood the basic difference between **Continuous Integration (CI)** and **Continuous Deployment (CD)** and how GitHub Actions can help automate the deployment workflow.

* * *

***Thanks for reading to the end; I hope you gained some knowledge.❤️🙌***

[**Linkedln**](https://www.linkedin.com/in/vishesh-ghule/)

[**Twitter**](https://twitter.com/VisheshGhule)

[**Github**](https://github.com/VisheshGhule)

[**Youtube Reference**](https://www.youtube.com/watch?v=y7S2oSjJ8PA&list=LL&index=3)
