Guide Intermediate 20 min read

GitHub Actions CI/CD Pipeline for Static Websites & Web Apps

A comprehensive guide to setting up automated CI/CD pipelines using GitHub Actions. Covers deployment, caching, multi-environment deployments, and secrets management.

OceanSoft Solutions
devopsgithub-actionsci-cddeployment
user@github

Why CI/CD?

Continuous Integration and Continuous Deployment (CI/CD) fundamentally alters how teams ship code. Manual deployments (e.g., using FileZilla, Cyberduck, or manual scp/rsync scripts directly from a developer's machine) introduce immense risk.

By utilizing GitHub Actions, you remove the human element from deploying code. You ensure that every piece of software pushed to your main branch is predictably built, tested, and shipped exactly the same way, every single time.

Understanding GitHub Actions

GitHub Actions relies on Workflows. A workflow is an automated procedure added to your repository defined via a YAML file inside the .github/workflows/ directory.

A typical workflow consists of:

  1. Events (on): Triggers that start the workflow (e.g., pushing code, opening a PR, scheduled runs).
  2. Jobs: A set of steps executed on the same runner (a virtual machine).
  3. Steps: Individual tasks that either run a shell command or an Action (pre-built executable scripts provided by the community).

Basic Production Deployment Pipeline

We'll start by building a pipeline that compiles a static site (using Nuxt, Next.js, Eleventy, or Astro) and ships it securely via Rsync to your Linux server (Ubuntu/Debian).

Create a file named .github/workflows/deploy.yml:

name: Deploy Production Site

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
      # Step 1: Clone the repository to the GitHub Actions runner
      - name: Checkout Code
        uses: actions/checkout@v4

      # Step 2: Setup Node Environment
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm' # Implicitly caches node_modules

      # Step 3: Install dependencies exactly as dictated by package-lock.json
      - name: Install Dependencies
        run: npm ci

      # Step 4: Run the build script
      - name: Build Site
        run: npm run build

      # Step 5: Transfer the built files to your secure server
      - name: Deploy to Server via Rsync
        uses: easingthemes/ssh-deploy@main
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
          ARGS: "-rlgoDzvc -i --delete"
          SOURCE: "dist/"
          REMOTE_HOST: ${{ secrets.SERVER_HOST }}
          REMOTE_USER: ${{ secrets.SERVER_USER }}
          TARGET: "/var/www/html/mysite/"

Required GitHub Secrets

Before this workflow succeeds, securely store the following in your repository (Settings > Secrets and variables > Actions):

  • SERVER_HOST: The IP or DNS of your target server (e.g., 203.0.113.50).
  • SERVER_USER: The SSH username (usually ubuntu, debian, or root).
  • SERVER_SSH_KEY: The private key corresponding to the public key installed in ~/.ssh/authorized_keys on your target server.

Advanced: Caching and Environments

For enterprise architectures, compiling code and transferring files is just the beginning.

Multi-Environment Pipelines (Staging vs Prod)

You shouldn't push directly to production. Instead, create pipelines that deploy to a Staging server when code hits the develop branch, and to Production when code is merged into main.

name: Continuous Deployment

on:
  push:
    branches:
      - main
      - develop

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Build Application
        run: |
          npm ci
          npm run build
        env:
          # This injects environment-specific variables defined in GitHub Environments
          API_ENDPOINT: ${{ vars.API_ENDPOINT }}

      - name: Execute Remote SSH Commands
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /var/www/${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
            git pull origin ${{ github.ref_name }}
            npm ci
            pm2 restart app

Implementing Dependency Caching

If your application takes 10 minutes to build and download packages, you are wasting GitHub Actions minutes and greatly reducing iteration speed. Utilize actions/cache:

      - name: Cache Node Modules
        id: cache-node-modules
        uses: actions/cache@v3
        with:
          path: node_modules
          key: node-modules-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            node-modules-

      - name: Install dependencies (if not cached)
        if: steps.cache-node-modules.outputs.cache-hit != 'true'
        run: npm ci

Note: In newer versions of setup-node, providing cache: 'npm' does this automatically, but explicitly using actions/cache gives you granular control over exactly what folders get cached (like .next cache folders).

Troubleshooting Tips

  • Permission Denied (publickey): Ensure the SERVER_SSH_KEY includes the -----BEGIN OPENSSH PRIVATE KEY----- wrapper. Additionally, log into your server and run cat ~/.ssh/authorized_keys to ensure the public counterpart is correctly saved on a single line.
  • Rsync Pathing Issues: Notice that SOURCE parameter (SOURCE: "dist/") in the rsync action. A trailing slash indicates you want the contents of the dist folder. Without the trailing slash, you will transfer the dist folder itself into the target directory, resulting in /var/www/html/mysite/dist/index.html which breaks your web server configuration.
  • npm install vs npm ci: Always use npm ci in CI/CD pipelines. It is strictly faster and installs exactly what is in your package-lock.json, throwing an error if the package file and log file are out of sync. npm install silently resolves versions and edits the lockfile.

Final Thoughts

Adopting GitHub Actions requires minimal setup, but the return on investment regarding application stability is huge. By ensuring tests are passed and processes are repeatable, your team can merge pull requests with confidence.

OceanSoft Solutions creates tailored DevOps and automation architectures for web platforms. Need help configuring a robust enterprise pipeline? Reach out to our consulting team.