Setting up a solid CI/CD pipeline for a Node.js application is no longer optional in 2026. Whether you are shipping a REST API, a microservice, or a full-stack app, automating your tests and deployments saves hours every week and prevents bugs from reaching production.
In this tutorial, we walk you through a complete, copy-paste friendly GitHub Actions workflow for a Node.js project. Every stage is explained: install, lint, test, build, and deploy. By the end, you will have a production-ready pipeline running on every push and pull request.
Why GitHub Actions for a Node.js CI/CD Pipeline?
There are many CI/CD tools available (Jenkins, CircleCI, GitLab CI, Buddy), but GitHub Actions has become the default choice for most Node.js teams. Here is why:
- Native integration with your GitHub repository, no extra account or token juggling
- Free tier generous enough for most small and medium projects (2,000 minutes/month for private repos, unlimited for public)
- Massive marketplace of pre-built actions (setup-node, cache, deploy scripts, etc.)
- YAML-based configuration that lives inside your repo and is versioned with your code
- Matrix builds to test against multiple Node.js versions in parallel

Prerequisites
Before we jump into the workflow file, make sure you have:
- A Node.js project pushed to a GitHub repository
- A
package.jsonwith scripts forlint,test, andbuild - Node.js 20 or 22 installed locally (both are LTS in 2026)
- A deployment target (we will use a generic SSH server example, but the same approach works for Vercel, AWS, Render, or Fly.io)
Project Structure
Your repository should look roughly like this:
my-node-app/
├── .github/
│ └── workflows/
│ └── ci-cd.yml
├── src/
├── tests/
├── package.json
├── package-lock.json
└── .eslintrc.json

The Complete GitHub Actions Workflow File
Create the file .github/workflows/ci-cd.yml and paste the following content:
name: Node.js CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
name: Build and Test
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
env:
CI: true
- name: Build application
run: npm run build
- name: Upload build artifact
if: matrix.node-version == '22.x'
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
deploy:
name: Deploy to Production
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/my-node-app
git pull origin main
npm ci --production
pm2 restart my-node-app
Stage-by-Stage Explanation
1. Trigger Configuration
The on block defines when the pipeline runs. Here it triggers on every push to main and on every pull request targeting main. This gives you fast feedback on PRs without deploying, and full CI/CD on merge.
2. Install Stage
We use actions/setup-node@v4 with a matrix strategy so tests run against Node.js 20 and 22 in parallel. The cache: 'npm' option caches your node_modules based on package-lock.json, drastically reducing install time on subsequent runs.
We use npm ci instead of npm install because it is faster and guarantees a clean, reproducible install from the lockfile.
3. Lint Stage
Running the linter early catches style and quality issues before you waste minutes on tests. Make sure your package.json has:
"scripts": {
"lint": "eslint . --ext .js,.ts"
}
4. Test Stage
The CI=true environment variable tells frameworks like Jest and Vitest to run in non-interactive mode. If you use code coverage, this is a good place to upload it to Codecov or Coveralls.
5. Build Stage
Whether you use TypeScript, Webpack, esbuild, or Vite, the build stage produces your production artifacts. We upload them with actions/upload-artifact@v4 so the deploy job can grab them without rebuilding.
6. Deploy Stage
The deploy job only runs when code is pushed to main (not on PRs) and only after build-and-test succeeds thanks to the needs keyword. The appleboy/ssh-action connects to your server and runs the deployment script.

Setting Up Secrets in GitHub
Never commit credentials to your repo. Instead, go to your repository Settings > Secrets and variables > Actions and add:
| Secret Name | Purpose |
|---|---|
SSH_HOST |
IP or domain of your server |
SSH_USER |
SSH username (e.g. deploy, ubuntu) |
SSH_PRIVATE_KEY |
Private SSH key with access to the server |
Alternative Deployment Targets
The SSH deploy step can easily be swapped depending on where you host your Node.js app:
- Vercel: use the official
amondnet/vercel-action - AWS Elastic Beanstalk: use
einaregilsson/beanstalk-deploy - Docker + Kubernetes: build an image, push to a registry, then run
kubectl set image - Render / Railway / Fly.io: trigger deploy hooks with a simple
curlcommand - Firebase / Cloud Run: use Google’s official
google-github-actionssuite

Best Practices for a Production-Ready Pipeline
- Pin action versions (use
@v4, not@main) to avoid surprise breakages - Cache aggressively for npm, Docker layers, and build outputs
- Fail fast: order stages so cheap checks (lint) run before expensive ones (integration tests)
- Use environments in GitHub Actions to require manual approval before production deploys
- Add status badges to your README so contributors see build health at a glance
- Monitor deployment health with a post-deploy smoke test or a healthcheck endpoint
- Rotate secrets regularly and use OIDC when possible instead of long-lived keys
Common Pitfalls to Avoid
- Running
npm installinstead ofnpm ciin CI (slower and less deterministic) - Forgetting to set
fetch-depth: 0when your build needs full git history - Storing secrets in
envblocks in the YAML file instead of GitHub Secrets - Not separating the deploy job from the test job, causing production deploys on failing branches
- Skipping the lockfile commit, which breaks reproducible builds
FAQ
How long does a typical Node.js CI/CD pipeline take?
With caching enabled, a well-optimized pipeline for a medium-sized Node.js app runs in 2 to 4 minutes end to end. Without caching, expect 5 to 10 minutes.
Can I use this workflow with TypeScript?
Yes. Just make sure your build script runs tsc or your bundler of choice, and that your test script uses a TypeScript-aware runner like Vitest, Jest with ts-jest, or Node.js native test runner with tsx.
Should I test against multiple Node.js versions?
If you are publishing a library, absolutely yes. Use the matrix strategy with the currently supported LTS versions (20 and 22 in 2026). For internal apps deployed on a single Node version, testing against just that version is enough.
How do I handle database migrations in the pipeline?
Add a dedicated step in the deploy job that runs your migration command (e.g. npx prisma migrate deploy or npx knex migrate:latest) before restarting the application process.
What is the difference between CI and CD?
Continuous Integration (CI) automates code checks: install, lint, test, build. Continuous Deployment (CD) automates delivery to environments (staging, production). A complete CI/CD pipeline combines both in a single automated workflow.
Is GitHub Actions free for private repos?
GitHub offers 2,000 free minutes per month for private repos on the Free plan, 3,000 on Pro, and unlimited minutes for public repositories. Most small to medium Node.js projects stay well within the free tier.
Wrapping Up
You now have a complete, production-grade CI/CD pipeline for your Node.js application using GitHub Actions. The workflow covers every critical stage: dependency installation, linting, testing across multiple Node versions, building, and safe deployment to production.
Copy the YAML file, adapt the deploy step to your hosting provider, add your secrets, and push to main. Your team will thank you for the fast feedback loop and the confidence of automated deployments.

