TL;DR: GitHub Actions is GitHub's built-in CI/CD system. You define workflows as YAML files that run on push, pull request, or on a schedule. In this article, I'll walk you through everything: from your first workflow to matrix builds, Docker images, self-hosted runners, and reusable workflows.

Imagine pushing code and everything just happens automatically: linting, testing, building, deploying. No more manual fiddling. Sounds great? That's exactly what CI/CD does — and GitHub Actions is the easiest way to get there.

🤔 What Is CI/CD and Why Should You Care?

CI (Continuous Integration) means your code gets automatically tested on every push. CD (Continuous Deployment/Delivery) ensures that tested code gets automatically deployed. Together, they form the backbone of modern software development.

Without CI/CD, here's what happens: you forget to run tests. A colleague merges broken code. Deployment becomes a Friday afternoon nightmare. With CI/CD? Push, sit back, done.

GitHub Actions is built right into GitHub — no external service, no extra setup. All you need is a YAML file in your repository.

GitHub Actions Documentation
Automate, customize, and execute your software development workflows right in your repository with GitHub Actions.

🧩 Core Concepts: Workflows, Jobs, Steps, Runners

Before we dive in, a quick overview of the building blocks:

  • Workflow: A YAML file under .github/workflows/. Defines when and what happens.
  • Job: A group of steps that run on the same runner. Jobs can run in parallel or sequentially.
  • Step: A single command or action. Steps run one after another within a job.
  • Runner: The machine where your job executes. GitHub provides free runners (Ubuntu, Windows, macOS) — or you can use your own.
  • Action: A reusable building block. Thousands of them are available on the GitHub Marketplace.
GitHub Marketplace - Actions
Thousands of pre-built actions for every use case.

🚀 Your First Workflow: Lint and Test on Push/PR

Let's start simple. Create the file .github/workflows/ci.yml in your repository:

name: CI

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

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm test

That's it. On every push to main and every pull request, linting and tests run automatically. You'll see the green check or red X right in the PR.

Git: History, Use, and Benefits
Everything about Git — the foundation for GitHub Actions.

🔢 Matrix Builds: Test Across Multiple Node.js Versions

Want to make sure your code works on different Node.js versions? Matrix builds make it trivial:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - run: npm ci
      - run: npm test

GitHub automatically creates three parallel jobs — one for each version. You can expand the matrix for different operating systems too:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node-version: [18, 20]
runs-on: ${{ matrix.os }}

That gives you 6 combinations. Automatically. No copy-paste.

⚡ Caching: Faster Builds Through Dependency Caching

Running npm ci every time is slow. Caching saves you precious minutes:

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'

- run: npm ci

The setup-node action has caching built right in. Alternatively, you can use the generic cache:

- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

First build? Normal speed. Every subsequent one? Noticeably faster. On large projects, this easily saves 2-3 minutes per run.

🔐 Secrets and Environment Variables

API keys, tokens, passwords — they don't belong in your code. GitHub Secrets are made for this:

# Set secrets via GitHub CLI
gh secret set MY_API_KEY --body "super-secret-value"
gh secret set DOCKER_PASSWORD --body "my-docker-password"

In your workflow, access them like this:

env:
  API_URL: https://api.example.com

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.MY_API_KEY }}
        run: |
          curl -H "Authorization: Bearer $API_KEY" $API_URL/deploy

Important: Secrets are automatically masked in logs. You'll only see *** instead of the actual value. And in pull requests from forks, secrets aren't available — an important security mechanism.

🐳 Building and Pushing Docker Images

Docker and GitHub Actions are a dream team. Here's how to build and push an image to Docker Hub or GitHub Container Registry:

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}

The beauty of it: GITHUB_TOKEN is provided automatically. No extra secret needed for GitHub Container Registry.

Docker: Easy Deployment of Services
Everything about Docker — perfect in combination with GitHub Actions.

🌐 Deployment: Vercel, Netlify, or via SSH

The build passed, tests are green — now ship it.

Vercel:

- name: Deploy to Vercel
  uses: amondnet/vercel-action@v25
  with:
    vercel-token: ${{ secrets.VERCEL_TOKEN }}
    vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
    vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
    vercel-args: '--prod'

SSH to your own server:

- name: Deploy via SSH
  uses: appleboy/ssh-action@v1
  with:
    host: ${{ secrets.SSH_HOST }}
    username: ${{ secrets.SSH_USER }}
    key: ${{ secrets.SSH_PRIVATE_KEY }}
    script: |
      cd /var/www/myapp
      git pull origin main
      npm ci --production
      pm2 restart myapp

Whether PaaS or bare metal — there's an action for everything.

♻️ Reusable Workflows and Composite Actions

Copy-pasting across multiple repos? No thanks. Reusable workflows solve this elegantly:

# .github/workflows/reusable-test.yml
name: Reusable Test Workflow
on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

Then call it like this:

# .github/workflows/ci.yml
jobs:
  test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '20'

Composite Actions are even more granular — perfect for reusable step groups that you want to share across repos.

🏗️ Practical Example: The Complete Pipeline

Now let's put it all together. A real pipeline: lint, test, build, deploy — with dependencies between jobs:

name: Full Pipeline

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

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - name: Deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          echo "Deploying build artifacts..."
          # Your deployment command here

Note the needs property: build waits for lint and test. Deploy waits for build. And deploy only runs on main with a push — not on pull requests. Clean.

Semantic Versioning: A Guide for Developers
Versioning and CI/CD go hand in hand — here's how.

🏠 Self-Hosted Runners for Your Homelab

GitHub's runners are great for most projects. But sometimes you need more control: specialized hardware, access to your local network, or simply unlimited build minutes.

# Set up a runner on your server
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64.tar.gz -L   https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf ./actions-runner-linux-x64.tar.gz
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO --token YOUR-TOKEN
./run.sh

In your workflow, just switch the runner:

jobs:
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - run: echo "Running on my own server!"

For homelab setups, this is gold. You can access local services directly, build Docker images locally, and deploy everything within your network.

💰 Pricing: What Does It Cost?

Plan Minutes/Month Storage Price
Free 2,000 500 MB $0
Team 3,000 2 GB $4/user/month
Enterprise 50,000 50 GB $21/user/month

Important: These minutes only apply to private repos. Public repos get unlimited minutes. And self-hosted runners don't consume any minutes — yet another reason for that homelab.

macOS runners cost 10x as much as Linux runners. Windows runners cost double. If you want to save: stick with Linux and only use macOS builds when absolutely necessary.

💡 Conclusion

GitHub Actions is a powerful tool that lives right inside your repository. No external service, no complicated setup. You write a YAML file and get automated tests, builds, and deployments in return.

Start small: a simple CI workflow with linting and tests. Then expand step by step — caching, matrix builds, Docker, deployment. And if you need more control, set up a self-hosted runner in your homelab.

The 2,000 free minutes per month are more than enough for most personal projects. And for open-source projects? Completely free. No more excuses for not using CI/CD.

Happy automating! 🚀

Artikel teilen:Share article: