How to Set Up CI/CD with GitHub Actions in 15 Minutes
🔍 Want the best deal? Check current prices and availability.
Compare Prices →When you buy through links on our site, we may earn a commission.
Introduction
You’ve just pushed a fix for that nasty bug. Now you wait. Wait for tests to run locally. Wait for the deploy script. Wait for the SSH connection. Wait for npm install to finish. By the time your fix is live, you could have built a whole new feature.
That’s where CI/CD comes in. And GitHub Actions is the easiest way to get it running — no separate server, no Jenkins, no $50/month DevOps tool. It’s baked right into every repository you already have.
In this tutorial, I’ll walk you through setting up a complete CI/CD pipeline using GitHub Actions. We’ll build, test, and deploy a simple Node.js app to production — all in about 15 minutes. By the end, every push to main will automatically ship your code. No more manual deploys, no more “it works on my machine.”
Let’s get started.
Prerequisites
Before we look at YAML, make sure you have these basics:
- A GitHub account – obviously. If you don’t have one, sign up for free.
- A repository with your code. I’ll use a simple Node.js Express app, but the same pattern works for Python, Go, static sites, or anything else.
- A hosting provider to deploy to. I’ll use Vercel for this example because it’s dead simple for frontend and Node.js apps. But you can swap in Railway, DigitalOcean App Platform, or even a plain VPS.
- Basic familiarity with the command line – we won’t go deep, but you should know what
npm installdoes.
That’s it. No Docker required, no Kubernetes, no PhD in YAML indentation.
Step 1: Create Your First Workflow File
GitHub Actions workflows are defined in .github/workflows/ inside your repository. Each file is a YAML document that describes one or more jobs.
Open your project in your editor (or GitHub’s web interface) and create a new file:
.github/workflows/deploy.yml
Here’s the skeleton we’ll build on:
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
- name: Deploy to Vercel
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: npx vercel --prod --token $VERCEL_TOKEN
Don’t worry if it looks like a lot — we’ll break down every line.
Why actions/checkout? This action pulls your code into the runner. Without it, the runner would have an empty filesystem.
Why actions/setup-node? It installs the exact Node.js version you specify. Other languages have similar actions: setup-python, setup-go, setup-java.
Why npm ci instead of npm install? npm ci is faster and respects your lockfile exactly. It’s the right choice for CI.
Commit this file to your main branch. GitHub will automatically detect it and run the workflow on the next push.
Step 2: Define Your Triggers (When to Run)
The on: section controls when your workflow fires. The most common triggers:
| Trigger | YAML | Use case |
|---|---|---|
| Push to branch | push: branches: [main] | Deploy on every commit to main |
| Pull request | pull_request: branches: [main] | Run tests before merging |
| Schedule (cron) | schedule: - cron: '0 6 *' | Nightly builds or cleanup |
| Manual trigger | workflow_dispatch: | Click a button in GitHub UI |
| Tag push | push: tags: ['v*'] | Release builds |
For a standard CI/CD pipeline, you’ll usually want:
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
This runs tests on every PR, and runs the full deploy only when merging to main. That way, you catch breakage before it hits production.
Pro tip: You can also use path filters to skip runs when only documentation changes:
on:
push:
branches: [ main ]
paths-ignore:
- 'docs/**'
- 'README.md'
Step 3: Add Build Steps
The steps list is where the real work happens. Each step runs sequentially. If one fails, the rest stop.
For a Node.js app, the build steps are straightforward:
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # cache node_modules between runs
- run: npm ci
- run: npm run build # if you have a build step
The cache: 'npm' line is a huge time-saver. It caches node_modules based on your package-lock.json. Subsequent runs skip the npm ci if nothing changed. For a typical app, this cuts the build time from 2 minutes to 30 seconds.
What about other languages?
- Python: Replace
setup-nodewithactions/setup-python@v5, thenpip install -r requirements.txt. - Go: Use
actions/setup-go@v5, thengo build ./.... - Static site (HTML/CSS/JS): You might not need a build step at all.
Step 4: Add Test Steps
Testing is the safety net of CI/CD. Without it, you’re just automating broken deploys.
Add a test step right after the build:
- run: npm test
env:
CI: true
The CI: true environment variable tells many test runners to treat warnings as errors and use CI-friendly output formatting.
If your tests require a database or other services, you can spin them up using service containers:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: testpass
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
env:
DATABASE_URL: postgres://postgres:testpass@localhost:5432/postgres
This is one of the most powerful features of GitHub Actions — you can test against real databases, Redis, or any Docker image without needing a separate CI server.
Honest take: Service containers are great, but they add ~30 seconds to startup time. For simple unit tests that don’t need a database, skip them. Your pipeline will be faster.
Step 5: Deploy to Production
Now the fun part — getting your code live.
I’ll show you how to deploy to Vercel because it’s the most popular choice for frontend and Node.js apps. But the pattern is similar for any platform.
Deploying to Vercel
- Get a Vercel token: Go to Vercel account settings and create a token.
- Add it as a GitHub secret: In your repo, go to Settings → Secrets and variables → Actions → New repository secret. Name it
VERCEL_TOKEN, paste the token. - Add the deploy step:
- name: Deploy to Vercel
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: npx vercel --prod --token $VERCEL_TOKEN
You can also use the official Vercel Action if you prefer a pre-built step.
Deploying to Railway
Railway is another great option, especially for full-stack apps. They have native GitHub integration, but you can also do it manually:
- name: Deploy to Railway
run: npx railway up --service ${{ secrets.RAILWAY_SERVICE }}
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
Deploying to DigitalOcean App Platform
DigitalOcean supports GitHub Actions via their CLI:
- name: Deploy to DigitalOcean
uses: digitalocean/app_action@v1
with:
app_name: my-app
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
Which one to pick? If your app is a static site or Next.js, Vercel is the smoothest. For backend-heavy apps with databases, Railway or DigitalOcean give you more control. I use Vercel for frontend and Railway for APIs.
Troubleshooting Common Issues
GitHub Actions is generally reliable, but here are the gotchas I’ve hit:
1. “No space left on device”
GitHub Actions runners have 14GB of storage. If you have large dependencies (e.g., Cypress, Chromium), you can hit the limit. Fix by:
- Using
actions/cachemore aggressively. - Removing unnecessary files with
rm -rfin a step. - Using a self-hosted runner if you really need more space.
2. Secrets not being passed
Double-check that your secret names match exactly. GitHub Actions secrets are case-sensitive. Also, secrets are only available to workflows triggered by push or pull_request from the same repository — not from forks (for security reasons).
3. Workflow not running
- Make sure the file is in
.github/workflows/(note thesinworkflows). - Check the branch name in your
on:trigger. If you push tomasterbut the trigger saysmain, it won’t run. - Look at the Actions tab in your repo. If the workflow is disabled, click “Enable workflow.”
4. npm ci fails with “package-lock.json not found”
You need a lockfile. Run npm install locally to generate one, then commit it. Alternatively, use npm install instead of npm ci, but that’s slower and less deterministic.
5. Build succeeds but deploy fails
Most deployment failures are due to missing environment variables or incorrect tokens. Check the deploy step’s logs in the Actions tab. If your hosting provider requires a specific file structure (e.g., vercel.json), make sure it’s committed.
Conclusion & Verdict
You now have a fully automated CI/CD pipeline. Every commit to main goes through: checkout → install → test → build → deploy. Total setup time: less than 15 minutes.
Is GitHub Actions the best CI/CD tool? For most solo developers and small teams, yes. It’s free for public repositories and gives you 2,000 minutes/month on private repos. That’s enough for hundreds of builds. The tight GitHub integration, massive action marketplace, and service containers make it unbeatable for the price.
Where it falls short: If you need to run Windows or macOS builds frequently, the free tier won’t cut it. Also, complex matrix builds can be hard to debug. For those cases, consider CircleCI or GitLab CI.
But for the 80% use case — build, test, deploy a web app — GitHub Actions is the clear winner. It’s already in your repo. No extra signups, no new dashboards to learn.
My recommendation: Start with the workflow we built here. Then add caching, parallel jobs, and maybe a staging environment. The GitHub Actions documentation is excellent when you’re ready to go deeper.
Now go ship that fix. Your CI/CD pipeline will handle the rest.
🔍 Want the best deal? Check current prices and availability.
Compare Prices →