How to Set Up a CI/CD Pipeline with GitHub Actions for Web Apps

One of the most popular services we offer is ongoing website maintenance because most clients we work with become return clients.

How to Set Up a CI/CD Pipeline with GitHub Actions for Web Apps

If you’re shipping a web application in 2026, manual builds and FTP deploys are no longer acceptable. A solid GitHub Actions CI/CD pipeline is the modern baseline: every push runs your tests, every merge produces a deployable artifact, and every release lands in production without human error.

In this tutorial, we won’t just talk theory. We’ll build a complete, copy-paste-ready workflow for a typical Node.js web app, explain each job line by line, and call out the pitfalls that trip up most teams. By the end, you’ll have a pipeline that lints, tests, builds, and deploys, all triggered by a single git push.

What is a GitHub Actions CI/CD Pipeline?

GitHub Actions is GitHub’s native automation platform. It lets you define workflows in YAML files stored in your repository under .github/workflows/. Each workflow can run on events like push, pull_request, schedules, or manual triggers.

A CI/CD pipeline built on GitHub Actions typically combines:

  • Continuous Integration (CI): automatically lint, test, and build your code on every change.
  • Continuous Delivery / Deployment (CD): automatically ship the built artifact to staging or production.

Unlike Jenkins or CircleCI, there’s no separate server to maintain. Runners are provided by GitHub (or self-hosted if you need it), and pricing is generous for public repos and reasonable for private ones.

github actions workflow

Prerequisites

Before we write any YAML, make sure you have:

  • A Node.js web app (Express, Next.js, NestJS, Fastify, etc.) hosted on GitHub.
  • A package.json with lint, test and build scripts.
  • Node 20 or 22 locally (we’ll target Node 22 in the pipeline, which is the current active LTS in 2026).
  • A deployment target. We’ll use a generic SSH deploy, but the same logic applies to Vercel, AWS, Azure, Fly.io or any container registry.

Step 1: Create the Workflow File

In your repo, create the following file:

.github/workflows/ci-cd.yml

GitHub automatically picks up any YAML file in this directory. You can have multiple workflows (one for CI, one for releases, one for nightly jobs), but for this tutorial we’ll keep everything in a single pipeline with multiple jobs.

github actions workflow

Step 2: The Full Working YAML

Here’s the complete pipeline. We’ll break it down right after.

name: CI/CD Pipeline

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

env:
  NODE_VERSION: '22'

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  test:
    name: Test
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 7

  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment:
      name: production
      url: https://yourapp.example.com
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - name: Deploy via SSH
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: 'dist/*'
          target: '/var/www/app'
      - name: Restart application
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: pm2 reload app

Step 3: Understanding Each Job

The on trigger

We run the pipeline on pushes to main and develop, and on pull requests targeting main. This gives you fast feedback during code review without wasting minutes on feature branch pushes.

The lint job

Linting fails fast and cheap. Always make it the first gate. Note the cache: 'npm' on setup-node: this caches your node_modules based on package-lock.json, often cutting install time from 60s to 10s.

The test job

Tests run only if linting passes, thanks to needs: lint. We upload the coverage report as an artifact so you can download it from the Actions tab or wire it to a coverage service later.

The build job

The build produces the deployable output (typically in dist/ or .next/). Uploading it as an artifact means the deploy job downloads the exact same build that was tested, instead of rebuilding and risking drift.

The deploy job

This is where CD happens. Three critical things:

  1. The if condition restricts deployment to pushes on main. PRs and develop pushes will not deploy.
  2. The environment block ties the job to a GitHub Environment, which lets you require manual approval, restrict secrets, and track deployment history.
  3. Credentials come from GitHub Secrets, never from the YAML itself.
github actions workflow

Step 4: Configure Secrets and Environments

Go to your repo settings:

  1. Settings > Secrets and variables > Actions: add SSH_HOST, SSH_USER, and SSH_PRIVATE_KEY.
  2. Settings > Environments: create an environment named production. Optionally enable Required reviewers so a teammate must approve before deploy.

For the SSH key, generate a dedicated deploy key (ssh-keygen -t ed25519 -C "github-actions-deploy") and add the public key to ~/.ssh/authorized_keys on your server. Never reuse your personal key.

Step 5: Add a Status Badge

Show off your green pipeline in your README:

![CI/CD](https://github.com/your-org/your-repo/actions/workflows/ci-cd.yml/badge.svg)
github actions workflow

Common Pitfalls to Avoid

After helping dozens of teams set up GitHub Actions pipelines, these are the mistakes we see again and again:

Pitfall Why it hurts Fix
Using npm install instead of npm ci Mutates lockfile, non-reproducible builds Always npm ci in CI
Rebuilding in the deploy job You deploy a build that was never tested Upload/download artifacts between jobs
Hardcoding secrets in YAML Massive security risk Use secrets.* exclusively
No caching Pipelines take 5x longer, waste minutes Enable cache: 'npm' in setup-node
Deploying on every branch push Accidental production deploys Guard with if: github.ref == 'refs/heads/main'
Pinning actions to @master Supply chain risk, surprise breakage Pin to a version tag or SHA
No timeouts A hung job can eat your monthly minutes Set timeout-minutes per job

Going Further

Once your basic pipeline is green, you can level it up:

  • Matrix builds: test on multiple Node versions in parallel with a strategy.matrix.
  • Docker: replace the SSH deploy with a build-and-push to GitHub Container Registry, then pull on your server.
  • Preview environments: spin up a temporary URL for every PR using Vercel, Netlify or a custom ephemeral environment.
  • Security scanning: add github/codeql-action and npm audit as separate jobs.
  • Slack/Discord notifications: notify your team on success or failure of production deploys.

FAQ

Is GitHub Actions a real CI/CD pipeline?

Yes. GitHub Actions is a fully featured CI/CD platform that handles build, test, and deployment automation, with the added benefit of being natively integrated into the repository where your code lives.

Is GitHub Actions free?

It’s free and unlimited for public repositories. Private repositories get a monthly allowance of free minutes depending on your plan, then it’s pay-as-you-go. Self-hosted runners are always free in terms of GitHub minutes.

How long should a CI/CD pipeline take?

For a typical Node.js web app, aim for under 5 minutes end to end. With proper caching and parallel jobs, most teams hit 2 to 3 minutes. Anything over 10 minutes will hurt developer velocity.

Can I use GitHub Actions to deploy to AWS, Azure, or GCP?

Absolutely. All three clouds publish official actions, and OIDC federation lets you authenticate without storing long-lived credentials as secrets, which is the recommended approach in 2026.

Should I use one workflow file or many?

Start with one. Split into multiple files only when you have clearly distinct triggers (for example, a nightly security scan or a release-on-tag workflow). Too many workflows become hard to reason about.

Wrapping Up

A solid GitHub Actions CI/CD pipeline is one of the highest-leverage investments you can make in a codebase. The YAML above is production-ready: copy it, swap in your scripts and secrets, and you’re shipping in minutes instead of hours.

At Pixelseed, we set up pipelines like this every week for our clients’ web apps. If you want a tailored review of your DevOps setup, get in touch and we’ll help you ship faster, safer, and with more confidence.

Subscription Form

Contact Details

Quick Links

Copyright © 2022 Pixel Seed. All Rights Reserved.