Setting Up GitHub Actions for Node.js CI/CD Pipeline
Comprehensive guide to automating testing, building, and deployment of Node.js applications using GitHub Actions with Docker and production workflows.
Setting Up GitHub Actions for Node.js CI/CD Pipeline
GitHub Actions provides free CI/CD automation directly in your repository. This guide walks through creating a production-ready pipeline that tests, builds, and deploys Node.js applications with Docker.
Why Automate With GitHub Actions?
Manual deployments are error-prone and slow. GitHub Actions runs on every code push, automatically running tests, building Docker images, and deploying to production. This ensures code quality and reduces deployment overhead.
Prerequisites
- GitHub repository with Node.js application
- Docker Hub or container registry account
- VPS with Docker and SSH access
- Node.js application with package.json and tests
Step 1: Create Workflow Directory
Create the GitHub Actions workflows directory:
mkdir -p .github/workflows
This directory stores all CI/CD workflow files.
Step 2: Create Test Workflow
Create .github/workflows/test.yml:
name: Run Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Use 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 --if-present
- name: Run type check
run: npm run type-check --if-present
- name: Run tests
run: npm test
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb
NODE_ENV: test
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./coverage/coverage-final.json
flags: unittests
name: codecov-umbrella
Step 3: Create Build and Push Workflow
Create .github/workflows/build-push.yml:
name: Build and Push Docker Image
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: docker.io
IMAGE_NAME: yourusername/myapp
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
Step 4: Create Deployment Workflow
Create .github/workflows/deploy.yml:
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to VPS
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
VPS_PRIVATE_KEY: ${{ secrets.VPS_PRIVATE_KEY }}
VPS_PORT: ${{ secrets.VPS_PORT }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
mkdir -p ~/.ssh
echo "$VPS_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -p $VPS_PORT -H $VPS_HOST >> ~/.ssh/known_hosts
ssh -p $VPS_PORT $VPS_USER@$VPS_HOST << 'EOF'
cd /app
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
docker pull yourusername/myapp:main
docker-compose down
docker-compose up -d
docker exec myapp npm run migrate
EOF
- name: Health check
run: |
for i in {1..30}; do
curl -f https://api.example.com/health && exit 0 || sleep 1
done
exit 1
Step 5: Configure Secrets
In GitHub repository, go to Settings > Secrets and variables > Actions.
Add these secrets:
| Secret | Value |
|---|---|
DOCKERHUB_USERNAME |
Your Docker Hub username |
DOCKERHUB_TOKEN |
Docker Hub access token |
VPS_HOST |
VPS IP address or hostname |
VPS_USER |
SSH username |
VPS_PRIVATE_KEY |
SSH private key (full content) |
VPS_PORT |
SSH port (usually 22) |
Generate SSH key pair (if you don't have one):
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions
cat ~/.ssh/github_actions
# Copy the output to VPS_PRIVATE_KEY secret
Add public key to VPS:
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys
Step 6: Create Dockerfile
Create Dockerfile:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
CMD ["node", "dist/index.js"]
Step 7: Create docker-compose.yml
Create docker-compose.yml for production:
version: '3.8'
services:
app:
image: yourusername/myapp:main
container_name: myapp
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- JWT_SECRET=${JWT_SECRET}
- API_KEY=${API_KEY}
networks:
- app-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 40s
nginx:
image: nginx:alpine
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- app
networks:
- app-network
networks:
app-network:
driver: bridge
Step 8: Create Migration Script
Create scripts/migrate.js:
import { db } from '../src/database';
async function runMigrations() {
try {
console.log('Running migrations...');
// Your migration logic here
await db.migrate();
console.log('Migrations completed successfully');
process.exit(0);
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
}
}
runMigrations();
Add to package.json:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"test": "jest",
"lint": "eslint src",
"type-check": "tsc --noEmit",
"migrate": "ts-node scripts/migrate.js"
}
}
Step 9: Monitor Workflow Runs
View workflow status in GitHub:
- Go to your repository
- Click Actions tab
- Select a workflow to view details
- Click individual runs to see logs
Step 10: Advanced Features
Matrix Testing
Test against multiple environments:
strategy:
matrix:
node-version: [18.x, 20.x]
postgres-version: [14, 15]
include:
- node-version: 20.x
postgres-version: 15
coverage: true
Conditional Steps
Skip deployment on pull requests:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
Notifications
Slack notification on failure:
- name: Notify Slack
if: failure()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
Best Practices
- Never commit secrets: Always use GitHub Secrets
- Use caching: Cache node_modules and Docker layers
- Run tests on PR: Test before merging to main
- Semantic versioning: Tag releases with versions
- Monitor workflows: Set up Slack/email notifications
- Document deployments: Add README with deployment instructions
| Workflow Stage | Action | Purpose |
|---|---|---|
| Trigger | Push to main/dev | Initiate pipeline |
| Test | Jest/Vitest | Run unit tests |
| Build | Docker build | Create container image |
| Push | Docker Hub/ECR | Store image registry |
| Deploy | VPS SSH | Update production |
Troubleshooting
Workflow not triggering: Check branch names and push events in workflow yaml
Docker authentication fails: Verify DOCKERHUB_TOKEN is valid (not password)
SSH connection timeout: Ensure VPS firewall allows GitHub Actions IPs
Database connection errors: Check services section and environment variables
Useful Resources
Conclusion
You now have a fully automated CI/CD pipeline that tests, builds, and deploys your Node.js application. This eliminates manual steps and ensures consistent, reliable deployments to production.