Private: Blog – Old

January 29, 2025 Company, Development 0 Comments

In today’s fast-paced development environment, deploying applications efficiently and reliably is essential. Manual deployments are time-consuming and prone to errors, making Continuous Integration and Continuous Deployment (CI/CD) practices essential. By automating build, test, and deployment processes, developers can ship code faster and with greater confidence.

This article walks through setting up a CI/CD pipeline using GitHub Actions to automate the deployment of a Node.js Express app. We’ll configure it to automatically build, publish a Docker image, and deploy the app to a remote server with minimal intervention. This setup enables you to focus on code improvement while ensuring updates reliably reach production.

The Problem:

For teams managing multiple applications, manual deployments can become a bottleneck, especially when multiple branches, environments, and code versions need frequent updates. A CI/CD pipeline using GitHub Actions helps streamline this process by automatically handling builds, Docker image creation, cleanup of old deployments, and final deployment to the server.

By the end of this article, you’ll have a functional pipeline that not only builds and packages your Express app into a Docker container but also deploys it to a live server, ensuring a smooth, continuous flow from code to production.

Prerequisites:

Before starting, ensure you have the following:

• EC2 Ubuntu Machine (or another Linux server) with Docker installed and SSH access configured.
• GitHub Repository with a master branch and optionally a development branch.
• Docker Hub Account for storing Docker images.
• Basic familiarity with GitHub Actions and Docker.

Setting Up the Project

We’ll use a simple Express application as the basis of our CI/CD pipeline. This app will serve as a lightweight example, but the setup can be applied to any Node.js application.

1. Creating a Basic Express App

Create a new directory for your project and initialize it:

mkdir cicd-express-app 
cd cicd-express-app 
npm init -y 

nstall Express:

npm install express

Create an index.js file for the app:

const express = require('express') 
const app = express() 
const port = 3000 
app.get('/', (req, res) => { 
res.send('Hi There, This application is deployed on EC2 using GitHub Acton! ') 
}) 
app.listen(port, () => { 
console.log(`Example app listening on port ${port}`) 
})

To test, run the app with:

node index.js 

You should see Hello from Express app! on http://localhost:3000.

2. Adding a Dockerfile

To containerize the application, create a Dockerfile:

# Use an official Nde.js runtime as a parent image  
FROM node:14  
# Set the working directory  
WORKDIR /app  
# Copy package.json and install dependencies  
COPY package*.json ./  
RUN npm install  
# Copy the rest of the app’s code  
COPY . .  
# Expose the port  
EXPOSE 3000  
# Command to run the app  
CMD ["node", "index.js"] 

To build and test the Docker image locally:

docker build -t my-express-app . 
docker run -p 3000:3000 my-express-app

Setting Up GitHub Actions Workflow

To automate the build, Dockerization, and deployment, we’ll use a GitHub Actions YAML configuration file. This file contains all the instructions that GitHub Actions needs to execute whenever you push code changes to the master branch.

  • Create a Workflow Directory:

In your project root, create a .github/workflows directory for GitHub Actions configurations.

mkdir -p .github/workflows
  • Add a Workflow File:

Create a file named ci-cd.yml in .github/workflows. This YAML file will define the steps of our CI/CD pipeline.

  • Define Triggers:

Set the workflow to trigger on pushes to the master, or any branch you want, branch.

name: CI/CD Pipeline 
on: 
 push: 
  branches: 
   - master
  • Set Up Build Job:

In the build job, we will set up Node.js, check out the code, and install dependencies.

jobs: 
 build: 
  runs-on: ubuntu-latest 
  steps: 
  - name: Checkout code 
    uses: actions/checkout@v2 
  - name: Set up Node.js 
    uses: actions/setup-node@v2 
    with: 
     node-version: '14' 
 - name: Install dependencies 
   run: npm install
  • Build and Publish Docker Image:

Log in to Docker Hub, build the Docker image, and push it to your Docker Hub repository.

- name: Log in to Docker Hub 
  uses: docker/login-action@v2 
  with: 
   username: ${{ secrets.DOCKERHUB_USERNAME }} 
   password: ${{ secrets.DOCKERHUB_TOKEN }} 
- name: Build and push Docker image 
  run: | 
   docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/my-express-app:latest . 
   docker push ${{ secrets.DOCKERHUB_USERNAME }}/my-express-app:latest
  • Clean Up Old Docker Containers:

Use SSH to connect to the server and remove old containers to avoid conflicts during deployment.

cleanDocker: 
  runs-on: ubuntu-latest 
  steps: 
  - name: Clean up old Docker containers 
    uses: appleboy/ssh-action@v0.1.1 
    with: 
     host: ${{ secrets.HOST_IP }} 
     username: ${{ secrets.SSH_USERNAME }} 
     key: ${{ secrets.HOST_SSH_PRIVATE_KEY }} 
     script: | 
      docker stop my-express-app || true 
      docker rm my-express-app || true
  • Deploy the New Docker Container:

Connect via SSH to the server, pull the latest Docker image, and run it.

deployDocker: 
  runs-on: ubuntu-latest 
  needs: cleanDocker 
  steps: 
  - name: Deploy new Docker container 
    uses: appleboy/ssh-action@v0.1.1 
    with: 
      host: ${{ secrets.HOST_IP }} 
      username: ${{ secrets.SSH_USERNAME }} 
      key: ${{ secrets.HOST_SSH_PRIVATE_KEY }} 
      script: | 
       docker pull ${{ secrets.DOCKERHUB_USERNAME }}/my-express-app:latest 
       docker run -d -p 80:3000 --name my-express-app ${{ secrets.DOCKERHUB_USERNAME }}/my-express-app:latest
Detailed YAML Configuration Walkthrough
  • on Trigger: The workflow runs whenever there’s a push to the master branch.
  • Build Job: Sets up Node.js, checks out the code, and installs and builds the project. It verifies the application’s compatibility before proceeding.
  • Docker Publish Job: This job logs into Docker Hub, builds the Docker image, and pushes it to Docker Hub.
  • Clean Docker Job: SSHes into the server to stop and remove any previous containers, preventing conflicts and freeing resources for the new deployment.
  • Deploy Docker Job: SSHes into the server, pulls the latest Docker image from Docker Hub, and runs it in detached mode (-d). It maps the app’s port 3000 to port 80, making it accessible publicly.
  1. Using appleboy/ssh-action for Remote Deployment

The appleboy/ssh-action GitHub Action is a powerful tool that lets you securely execute commands on a remote server over SSH directly from your GitHub Actions workflow. This action is particularly useful for deploying applications, managing infrastructure, or running maintenance scripts on remote servers without needing additional CI/CD tools.

Key Features:

  • Secure Remote Access: Establishes SSH connections with credentials stored in GitHub Secrets.
  • Command Execution: Allows any command to be run on the remote server, such as Docker commands, filesystem operations, and application startup scripts.
  • Flexible Authentication: Supports password-based, private key, and multi-server authentication configurations, making it adaptable for various deployment environments.

In this workflow, the appleboy/ssh-action action is used in both cleanDocker and deployDocker jobs to perform remote tasks like stopping and removing containers and running the latest Docker image on the server.

  • GitHub Secrets Management

Sensitive information, such as SSH credentials, Docker Hub credentials, and server IPs, are stored in GitHub Secrets to keep credentials secure. To set secrets:

  1. Go to your GitHub repository.
  2. Click on Settings > Secrets > Actions > New Repository Secret.
  3. Add secrets like DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, HOST_IP, and HOST_SSH_PRIVATE_KEY.

Testing the Workflow

Once configured, you can test the workflow by pushing a commit to the master branch. GitHub Actions should trigger and display the workflow’s progress under the Actions tab. After a successful workflow run:

  1. The app will be built and tested.
  2. A Docker image will be created and pushed to Docker Hub.
  3. The server will pull and run the new image, making your updates live.

Conclusion

With this setup, you have a functional CI/CD pipeline using GitHub Actions and Docker. By integrating automated builds, containerization, and secure deployment, you can deliver updates faster and more reliably. This approach saves time and ensures consistent, high-quality deployments across different environments.

This setup is easily adaptable, allowing you to apply it across multiple applications and environments as your projects grow.

Optional Reference Links:

GitHub: https://github.com/dibyenduswar/Automating-CI-CD-with-GitHub-Actions-and-Docker-for-a-Node.js-Express-App



Back to blog list

Tags



Comment

Conect With Us