Skip to main content

Command Palette

Search for a command to run...

Day 63: Building a CI-CD Pipeline with Docker & Github Actions

#90daysofdevops

Published
•5 min read
Day 63: Building a CI-CD Pipeline with Docker & Github Actions
V

I'm proficient in a variety of DevOps technologies, including AWS, Linux, Python, Docker, Git/Github, Shell Scripting, Jenkins and Computer Networking. My greatest strength is the ability to learn new things because I believe there is always room for self-development

🚀 Introduction

In this blog, I’ll walk you through how I built a CI/CD pipeline using GitHub Actions and Docker. The best part you don’t need any cloud setup to try this out. With just GitHub Actions, Docker, Docker Hub, and a local VM/Minikube, you can containerize your application, automate testing, and deploy locally. This project is beginner-friendly yet powerful enough to give you a real taste of DevOps in action.

Project Overview

This blog demonstrates how to build a complete CI/CD pipeline using Github action and Docker without relying on any cloud provider. The pipeline automates:

  • Building a Docker image

  • Running tests

  • Pushing the image to Docker Hub

  • Deploying it on a local VM (no Minikube, no cloud)

This is a clean, practical setup for those who want to implement containerized CI/CD workflows in fully controlled, local environments.

Tech Stack

  • GitHub Actions – For CI/CD automation.

  • Docker – Containerization platform.

  • Docker Hub (Free) – To store and distribute container images.

  • Local VM – For local deployment.

Workflow Overview

  1. Dockerize the App – Write a Dockerfile and docker-compose.yml.

  2. CI/CD Setup – Configure GitHub Actions to:

    • Run tests.

    • Build the Docker image.

    • Push the image to Docker Hub.

  3. Local Deployment – Run the image in a local VM.


🔸Create Docker file and docker-compose.yml file

  • Create Docker file
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
EXPOSE 3000
CMD ["npm", "start"]

This Dockerfile containerizes a Node.js application using the lightweight Node 18 Alpine image. It sets /app as the working directory, copies the package.json files to install only production dependencies using npm ci --omit=dev, and then copies the application’s source code from the src folder. The container exposes port 3000 and runs npm start when launched, creating a compact, production-ready environment for the app.

  • Create docker-compose.yml file
version: '3.9'
services:
  web:
    build: .
    ports:
      - "8080:3000"
    environment:
      - PORT=3000

This docker-compose.yml file sets up a single service called web that builds the Docker image from the current directory. It maps port 8080 on the host to port 3000 inside the container and sets the PORT environment variable to 3000, ensuring the app runs correctly. This makes it easy to start and manage the application using Docker Compose.

  • Create GitHub Actions workflow file
name: CI

on:
  push:
    branches: [ "main" ]
    tags: [ 'v*.*.*' ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v3
      - run: echo "Running build job on my Ubuntu machine"

  docker:
    needs: build
    runs-on: self-hosted
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ secrets.DOCKERHUB_USERNAME }}/ci-cd-docker-demo

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

      - name: Deploy container on local machine
        run: |
          echo "Stopping old container (if running)..."
          docker stop myapp || true
          docker rm myapp || true

          echo "Freeing up port 3000 if in use..."
          CONTAINER_ID=$(docker ps -q --filter "publish=3000")
          if [ ! -z "$CONTAINER_ID" ]; then
            echo "Another container is using port 3000. Stopping it..."
            docker stop $CONTAINER_ID || true
            docker rm $CONTAINER_ID || true
          fi

          echo "Pulling latest image..."
          docker pull ${{ steps.meta.outputs.tags }}

          echo "Running new container..."
          docker run -d --name myapp -p 3000:3000 ${{ steps.meta.outputs.tags }}

This GitHub Actions workflow automates building, pushing, and deploying a Dockerized app. It runs on pushes to the main branch, version tags, and pull requests. The workflow builds the Docker image, logs in to Docker Hub, pushes the image, and then deploys it on a local machine by stopping any old container on port 3000 and running the latest image, ensuring the app is always up-to-date automatically.


🔸Create Personal Access Token in Docker hub

  • Create Personal Access Token in Docker hub in setting

  • And save docker credential somewhere


🔸Add docker credentilas in your

  • Go to setting âž” secret and variables âž” action âž” add docker credentials


🔸Create self-hosted runner

  • Before creating self-hosted runner first understand what it is.

  • A self-hosted runner is a machine you manage yourself to run GitHub Actions workflows instead of using GitHub’s cloud runners. It’s used when you need more control over the environment, access private networks, install custom software, or deploy directly to local servers, making it ideal for projects like Docker-based CI/CD pipelines.

  • In setting create self-hosted runner to deploy container/run application on local machine (select your OS while creating self-hosted runner)

  • After creating self hosted runner, copy paste that command in your local VM. (After creating self-hosted runner it will give some command just copy paste it on your local VM as it is in sequence wise, like shown in below picture)


🔸Run all-job in action

  • Just go in action, click on ci-cd pipeline, and click on re-run all jobs. Follow below picture to understand it in better way.

  • In github âž” action âž” select pipeline and do run all jobs

  • In the above picture we can see that pipeline is running without any error

  • In Docker hub we can see that our application has been successfully pushed.

  • Check docker container is running & images in local VM by typing docker ps & docker images. As shown in below picture.

🔸Access application

  • Access application on browser by typing localhost:3000


🚀Conclusion

Setting up this CI/CD pipeline was a practical way to see automation come to life — from writing code to having a tested, containerized application running locally. By using GitHub Actions and Docker, I could create a workflow that builds, tests, and deploys seamlessly without relying on cloud infrastructure.

If you’re just starting out with DevOps, this is a great project to experiment with. Once you’re comfortable with the basics, you can easily extend the same pipeline to deploy on AWS, Azure, or GCP. For now, this setup proves that you don’t need heavy infrastructure to start practicing real-world DevOps workflows.


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

Linkedln

Twitter

Github