MHRubel
HomeAboutProjectsSkillsExperienceBlogContact
MHRubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
Software EngineeringTechnologyJune 18, 20266 min read

CI/CD with GitHub Actions for Scalable Web Apps

Master CI/CD with GitHub Actions for your web development projects. Learn how to build, test, and deploy scalable applications efficiently through automation.

DevOpsLinuxServer

CI/CD with GitHub Actions for Scalable Web Applications

If you're building web applications, you know that getting code from your local machine to production reliably and quickly is a huge challenge. That's where Continuous Integration and Continuous Deployment (CI/CD) come in. And lately, I've been really impressed with how powerful and flexible GitHub Actions can be for setting up these pipelines. It's not just for simple scripts anymore; we're talking about full-blown automation for complex, scalable web applications.

In this post, I want to walk you through how I approach building CI/CD pipelines with GitHub Actions, focusing on what it takes to make them work for applications that need to scale. We'll cover the core concepts, essential tools, and some practical examples.

Why GitHub Actions?

Before diving in, let's quickly touch on why GitHub Actions has become my go-to for CI/CD.

  1. Deep Integration: It's built right into GitHub. This means no external services to manage, simpler authentication, and a more streamlined developer experience.
  2. Flexibility: You can define workflows using YAML, which is human-readable and version-controlled alongside your code. You can trigger workflows on virtually any GitHub event.
  3. Community & Marketplace: There's a huge marketplace of pre-built actions for everything from setting up languages and frameworks to deploying to cloud providers. This saves a ton of time.
  4. Scalability: GitHub Actions runners can handle a decent load, and you can even self-host runners if you have specific performance or security needs.

Core Components of a CI/CD Pipeline

A typical CI/CD pipeline for a web application involves several stages:

  • Code Checkout: Getting the latest code from your repository.
  • Setup Environment: Installing dependencies, setting up the correct language version (e.g., Node.js, Python, Go).
  • Linting & Formatting: Checking code style and catching basic errors.
  • Testing: Running unit tests, integration tests, and potentially end-to-end tests.
  • Building: Compiling code, bundling assets, creating Docker images.
  • Security Scanning: Checking for vulnerabilities in dependencies or code.
  • Deployment: Pushing the built artifact to your staging or production environment.

Building a Basic CI Pipeline with GitHub Actions

Let's start with a simple CI pipeline. Imagine we have a Node.js web application. Our goal here is to automatically build and test the application whenever code is pushed to a feature branch or a pull request is opened.

Here's a basic .github/workflows/ci.yml file:

YAML
name: Node.js CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x, 18.x, 20.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
    - name: Checkout code
      uses: actions/checkout@v4 # Always good to use the latest version

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm' # Cache npm dependencies for faster builds

    - name: Install dependencies
      run: npm ci # Use 'npm ci' for cleaner, faster installs in CI

    - name: Run linters
      run: npm run lint

    - name: Run tests
      run: npm test

Explanation:

  • name: The name of your workflow.
  • on: Defines when this workflow runs. Here, it's on push or pull_request events to the main or develop branches.
  • jobs: A workflow can have multiple jobs. Here, we have one job named build.
  • runs-on: Specifies the type of runner to use. ubuntu-latest is a common choice.
  • strategy.matrix: This is super handy. It allows you to run the same job with different configurations. Here, we're testing our app against three different Node.js versions (16.x, 18.x, 20.x).
  • steps: A sequence of tasks to run within a job.
    • actions/checkout@v4: This action checks out your repository code so the workflow can access it. Using @v4 ensures you're on a recent, stable version.
    • actions/setup-node@v4: Sets up the specified Node.js version. The cache: 'npm' option is crucial for performance; it caches your node_modules directory, drastically speeding up subsequent runs.
    • npm ci: This command installs dependencies exactly as specified in package-lock.json. It's generally preferred over npm install in CI environments because it's faster and more reliable.
    • npm run lint and npm test: These steps execute your defined linting and testing scripts from package.json. If either of these fails, the job fails, preventing bad code from progressing.

Adding a Deployment Stage

Now, let's extend this to a CD pipeline. We want to deploy our application to a staging environment whenever we merge changes into the develop branch. For this example, let's assume we're deploying to a cloud platform like AWS Elastic Beanstalk, or perhaps a container registry like Docker Hub and then to Kubernetes.

We'll need a separate job for deployment, and it should only run after the build job on the develop branch succeeds.

YAML
name: Node.js CI/CD

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  build_and_test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16.x, 18.x, 20.x]

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

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run linters
      run: npm run lint

    - name: Run tests
      run: npm test

    - name: Build application
      run: npm run build # Assuming 'npm run build' creates production assets

    # Optional: Build and push Docker image
    # - 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
    #   uses: docker/build-push-action@v4
    #   with:
    #     context: .
    #     push: true
    #     tags: your-dockerhub-username/your-app:${{ github.sha }} # Tag with commit SHA

  deploy_staging:
    runs-on: ubuntu-latest
    needs: build_and_test # This job depends on build_and_test completing successfully
    if: github.ref == 'refs/heads/develop' && needs.build_and_test.result == 'success' # Only run on develop branch pushes that pass CI

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

    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18' # Use a consistent version for deployment

    - name: Install dependencies
      run: npm ci

    - name: Build application
      run: npm run build

    # --- Deployment Steps ---
    # Example for AWS Elastic Beanstalk (requires AWS CLI and EB CLI configured)
    # - name: Configure AWS Credentials
    #   uses: aws-actions/configure-aws-credentials@v1
    #   with:
    #     aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    #     aws-access-key-secret: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    #     aws-region: us-east-1

    # - name: Deploy to Elastic Beanstalk
    #   run: |
    #     EB_APP_NAME="your-eb-app-name"
    #     EB_ENV_NAME="your-staging-env-name"
    #     # Create application version and deploy
    #     eb deploy $EB_ENV_NAME --version-label $GITHUB_SHA --message "Deployment from GitHub Actions commit ${{ github.sha }}"

    # Example for deploying a Docker image to Kubernetes (using kubectl)
    # This is a simplified example; real-world K8s deployments often use Helm or other tools.
    #
Back to Blog