How to Set Up GitHub Actions for CI/CD (Beginner-Friendly Guide)
On this page
If you're still SSHing into servers to deploy, or running tests manually before merging PRs, you're burning time you'll never get back. I put off setting up CI/CD for way too long on personal projects because it felt like "extra work" — turns out the 15 minutes of setup saves hours every week.
GitHub Actions lets you automate your entire build-test-deploy pipeline directly from your repo. No Jenkins server to babysit, no CircleCI account to manage, and it's free for public repos with a generous free tier for private ones.
Let me walk you through setting it up from scratch.
TL;DR
- Workflow files live in
.github/workflows/— they're just YAML - Triggers:
push,pull_request, schedules, or manual dispatch - Free tier: 2,000 minutes/month for private repos, unlimited for public
- You can go from zero to working CI/CD in under 15 minutes
- Caching and matrix strategies keep builds fast at scale
What Is GitHub Actions?
It's GitHub's built-in CI/CD platform. Your workflow definitions live alongside your code, version-controlled and reviewable in PRs. No separate infrastructure to maintain.
It's event-driven: push code, run tests. Open a PR, lint and preview. Merge to main, deploy to production. You define these automations in YAML, GitHub runs them on hosted runners (Ubuntu, macOS, or Windows).
Over 90% of the top 1,000 GitHub repos use it as of 2025. For good reason — it just works.
Pricing
Public repos: completely free, unlimited minutes. Private repos: 2,000 minutes/month on Linux. Most small-to-medium teams never hit the limit.
Step 1: Your First Workflow
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
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"
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
Commit, push, done. Every push to main and every PR triggers this pipeline.
Breaking it down:
on— trigger events. Pushes and PRs to main.jobs— each job gets a fresh VM.runs-on—ubuntu-latestis cheapest and most common.steps— pre-built actions (uses) or shell commands (run).
Step 2: Add Caching
Without caching, npm ci downloads everything on every run. That's 30-90 seconds wasted.
jobs:
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 lint
- run: npm test
The cache: "npm" option caches your ~/.npm directory. Cuts install time by 50-70% on subsequent runs. One line, massive improvement.
Step 3: Matrix Strategy
Test across multiple Node versions:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm test
Three parallel jobs, one per version. Total pipeline time is roughly the same as a single run.
Step 4: Add Deployment
Deploy to production only on merge to main, only after tests pass:
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
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 lint
- run: npm test
- run: npm run build
deploy:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- run: npm run build
- name: Deploy to production
run: |
npx vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
Key bits:
needs: test— deploy waits for tests to pass.ifcondition — only deploys on pushes to main, not PR checks.secrets.VERCEL_TOKEN— sensitive values go in GitHub Secrets.
If you're deploying to AWS EC2 instead, check out our guide on deploying Node.js apps to EC2 which covers SSH-based deployments.
Step 5: Secure Your Secrets
Never hardcode credentials. GitHub Secrets are encrypted, only exposed during workflow runs, and masked in logs.
- Repo → Settings → Secrets and variables → Actions
- New repository secret
- Add keys like
VERCEL_TOKEN,AWS_ACCESS_KEY_ID - Reference as
${{ secrets.YOUR_SECRET_NAME }}
Pitfalls I've Hit
Running out of minutes
If your pipeline runs on every push to every branch, minutes vanish fast. Limit triggers:
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
Slow pipelines
A 15+ minute pipeline kills momentum. Quick wins:
- Cache deps — 30-90 seconds saved per run
npm ciovernpm install— faster and deterministic- Parallel jobs — lint and test don't need to be sequential
- Path filters — skip CI when only docs change:
on:
push:
branches: [main]
paths-ignore:
- "**.md"
- "docs/**"
Unpinned action versions
Don't use @main or @latest:
# Good
- uses: actions/checkout@v4
# Better (pinned to SHA)
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# Bad
- uses: actions/checkout@main
No environment protection
For production deploys, enable protection rules: manual approval, branch restrictions, wait timers. Settings → Environments.
Real-World: Docker + Postgres
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- run: npm test
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb
build-and-push:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
The services block spins up a real Postgres alongside your tests — one of GitHub Actions' most underrated features.
Actions Worth Knowing
actions/checkout@v4— every workflow needs thisactions/setup-node@v4— Node.js with built-in cachingactions/cache@v4— general-purpose cachingdocker/build-push-action@v6— Docker buildsgithub/codeql-action@v3— security scanningactions/upload-artifact@v4— store build outputs
Debugging Failures
- Click the failed run in the Actions tab
- Expand the failed step for logs
- Set
ACTIONS_RUNNER_DEBUGsecret totruefor verbose output - Use act to test workflows locally
For teams setting up more complex pipelines — multi-environment deploys, custom runners, IaC integration — Adaptels offers DevOps consulting.
Wrapping Up
GitHub Actions removes the friction between writing code and shipping it. Start with a test workflow, add caching, layer on deployment. You don't need the perfect pipeline on day one — iterate like you would any other code.
The configs in this guide are production-ready starting points. Copy them, adjust the Node version and deploy step, and you'll have CI/CD running in minutes.
Sources
- GitHub Actions Documentation — Official docs covering workflows, syntax, and runner specs
- GitHub Actions Billing and Usage — Free tier limits and pricing details
- actions/setup-node — Official Node.js setup action with built-in caching
- Docker Build Push Action — Official Docker action for building and pushing images
- GitHub Actions Security Hardening — Best practices for securing your CI/CD pipelines
Related Articles
How to Debug Node.js Memory Leaks (Step-by-Step Guide)
Learn how to detect, diagnose, and fix Node.js memory leaks using heap snapshots, Chrome DevTools, and clinic.js — with real code examples.
Running Local LLMs With Ollama: Developer Setup Guide
Set up Ollama to run local LLMs on your machine. Covers installation, model selection, API usage, and integrating local models into your dev workflow.
Python Virtual Environments Explained: venv vs conda vs pyenv
A practical comparison of Python's venv, conda, and pyenv — when to use each, how to set them up, and which one fits your workflow.