Back to Tutorials
Tutorial Devops

CI/CD Pipeline with GitHub Actions

This tutorial guides you through setting up a complete CI/CD pipeline using GitHub Actions, helping you automate your software deployment process. You'll learn key concepts, practical steps, and best practices while building a simple Node.js application, enabling you to streamline your development workflow effectively.

Difficulty
Beginner
Duration
45 minutes
Overview

Tutorial: CI/CD Pipeline with GitHub Actions

Learning Objectives and Outcomes

In this tutorial, you will learn how to:

  • Understand the fundamentals of Continuous Integration and Continuous Deployment (CI/CD).
  • Set up a CI/CD pipeline using GitHub Actions to automate your software deployment process.
  • Write and configure GitHub Actions workflows using YAML.
  • Build and push Docker images as part of your CI/CD pipeline.
  • Deploy your application automatically using GitHub Actions.
  • Manage secrets and environments securely in GitHub Actions.

By the end of this tutorial, you will have a working CI/CD pipeline integrated with your GitHub repository, enabling you to streamline your development process.

Prerequisites and Setup

Before diving into the tutorial, ensure you have:

  • Basic knowledge of Git: You should understand how to clone repositories, commit changes, and push to GitHub.
  • Basic knowledge of YAML: Familiarity with YAML syntax is essential for writing GitHub Actions workflows.
  • Docker installed locally: Required for building and testing container images.
  • A container registry account: Such as Docker Hub or GitHub Container Registry, for pushing images.

Setting Up Your Environment

  1. Create a GitHub Account: If you don't have one, sign up at GitHub.
  2. Create a Repository: Create a new repository where you will set up your CI/CD pipeline.
  3. Clone the repository locally and create a simple Node.js application to use throughout this tutorial.

Step-by-Step Instructions with Examples

Step 1: Workflow Basics

GitHub Actions workflows are YAML files stored in .github/workflows/ in your repository. Each workflow defines when to run and what to do.

  1. Create the workflows directory:
    mkdir -p .github/workflows
    
  2. Create .github/workflows/ci-cd.yml with a basic structure:
    name: CI/CD Pipeline
    
    on:
      push:
        branches:
          - main
      pull_request:
        branches:
          - main
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
  3. Key concepts in this file:
    • on: Defines the trigger events — here, pushes and pull requests to main.
    • jobs: Each job runs in its own environment. You can have multiple jobs.
    • runs-on: The operating system for the job runner (e.g., ubuntu-latest).
    • steps: Sequential tasks within a job, using either run (shell commands) or uses (pre-built actions).
  4. Commit and push this file. GitHub automatically detects it and displays it under the Actions tab.

Step 2: Build and Test

Extend your workflow to install dependencies, run tests, and produce a build artefact.

  1. Update the build job in your workflow file:
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Set up Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'npm'
    
          - name: Install dependencies
            run: npm ci
    
          - name: Run tests
            run: npm test
    
          - name: Build application
            run: npm run build
    
  2. The cache: 'npm' option caches your node_modules between workflow runs, speeding up subsequent builds.
  3. Use npm ci instead of npm install in CI pipelines — it installs exact versions from package-lock.json for reproducible builds.
  4. Push your changes. In the Actions tab, watch each step run in real time. If a test fails, the workflow stops and later jobs will not execute.

Step 3: Docker Build

After a successful build and test phase, build a Docker image and push it to a container registry.

  1. Create a Dockerfile in your repository root:
    FROM node:20-alpine
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    COPY . .
    EXPOSE 3000
    CMD ["node", "index.js"]
    
  2. Add a docker job that depends on the build job:
      docker:
        needs: build
        runs-on: ubuntu-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Log in to Docker Hub
            uses: docker/login-action@v3
            with:
              username: ${{ secrets.DOCKER_USERNAME }}
              password: ${{ secrets.DOCKER_PASSWORD }}
    
          - name: Build and push Docker image
            uses: docker/build-push-action@v5
            with:
              context: .
              push: true
              tags: ${{ secrets.DOCKER_USERNAME }}/my-app:latest,${{ secrets.DOCKER_USERNAME }}/my-app:${{ github.sha }}
    
  3. The needs: build directive ensures the Docker job only runs after the build and test job succeeds.
  4. Tagging with ${{ github.sha }} in addition to latest gives you an immutable image reference for each commit.

Step 4: Deployment

With your Docker image pushed to a registry, automatically deploy it to your server.

  1. Add a deploy job that depends on the docker job:
      deploy:
        needs: docker
        runs-on: ubuntu-latest
        environment: production
        steps:
          - name: Deploy to server
            uses: appleboy/ssh-action@v1
            with:
              host: ${{ secrets.DEPLOY_HOST }}
              username: ${{ secrets.DEPLOY_USER }}
              key: ${{ secrets.DEPLOY_SSH_KEY }}
              script: |
                docker pull ${{ secrets.DOCKER_USERNAME }}/my-app:${{ github.sha }}
                docker stop my-app || true
                docker rm my-app || true
                docker run -d \
                  --name my-app \
                  --restart unless-stopped \
                  -p 3000:3000 \
                  ${{ secrets.DOCKER_USERNAME }}/my-app:${{ github.sha }}
    
  2. The environment: production declaration links this job to a GitHub Environment, enabling protection rules such as required reviewers before deployment proceeds.
  3. Adapt the deployment script to your hosting target — for Kubernetes, use kubectl set image; for AWS ECS, use the AWS CLI or a dedicated action.

Step 5: Secrets and Environments

Secrets keep sensitive values out of your source code. GitHub Environments add approval gates and environment-scoped overrides.

  1. Add repository secrets:
    • Go to Settings → Secrets and variables → Actions → New repository secret.
    • Create secrets for: DOCKER_USERNAME, DOCKER_PASSWORD, DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY.
  2. Create a production Environment:
    • Go to Settings → Environments → New environment and name it production.
    • Enable Required reviewers to enforce a manual approval step before every production deployment.
    • Add environment-specific secrets here if they differ from repository-level secrets.
  3. Reference secrets in your workflow using ${{ secrets.SECRET_NAME }}. GitHub automatically masks them in job logs.
  4. Use environment variables for non-sensitive configuration shared across jobs:
    env:
      NODE_ENV: production
      PORT: 3000
    
  5. Never hardcode credentials, tokens, or connection strings in workflow files — always use secrets.

Key Concepts Explained Along the Way

  • Continuous Integration (CI): The practice of automatically testing and integrating code changes into a shared repository.
  • Continuous Deployment (CD): The process of automatically deploying code changes to production after passing tests.
  • Workflows: A YAML file that defines the automated processes in GitHub Actions.
  • Jobs: A collection of steps that run in the same environment. Jobs can run in parallel or be sequenced using needs.
  • Steps: Individual tasks executed within a job, using either run commands or reusable uses actions.
  • Secrets: Encrypted values stored in GitHub, injected into workflows at runtime and masked in logs.
  • Environments: Named deployment targets with optional protection rules and environment-specific secrets.

Common Mistakes and How to Avoid Them

  • Using npm install in CI: Use npm ci instead for reproducible, deterministic installs.
  • Not pinning action versions: Use actions/checkout@v4 (not @main) to avoid unexpected breaking changes.
  • Forgetting needs: Without needs, jobs run in parallel — always chain dependent jobs explicitly.
  • Incorrect branch name: Verify your workflow triggers on the correct branch (e.g., main not master).
  • Exposing secrets in logs: Never echo a secret value directly. GitHub only masks values referenced via ${{ secrets.NAME }}.

Exercises and Practice Suggestions

  • Add a matrix build to test against multiple Node.js versions simultaneously using strategy.matrix.
  • Configure a scheduled workflow (on: schedule) to run nightly integration tests.
  • Add a Slack or email notification step that fires when a deployment succeeds or fails.
  • Experiment with GitHub Environments by adding a required reviewer before the production deploy proceeds.

Next Steps and Further Learning

  • Explore advanced GitHub Actions features such as reusable workflows and composite actions.
  • Learn about deploying to managed cloud services (AWS ECS, Google Cloud Run, Azure Container Apps).
  • Add security scanning steps — use trivy for container image scanning and snyk for dependency auditing.

With these tools and knowledge, you can create powerful and efficient CI/CD pipelines using GitHub Actions, streamlining your development workflow and increasing your team's productivity.

Category

Devops

Prerequisites

  • github actions
  • docker
  • nodejs

Steps

  • 1
    Workflow Basics
  • 2
    Build and Test
  • 3
    Docker Build
  • 4
    Deployment
  • 5
    Secrets and Environments