Build a Complete CI/CD Pipeline Using GitHub Actions (Step-by-Step)
Welcome to the Cloudshalla Engineering Blog! We break down the real, unfiltered truths of DevOps, Cloud, and Platform Engineering fresh from the production trenches. If you are serious about stepping up your career, you are in exactly the right place.
What We're Building
A real, production-grade GitHub Actions pipeline with 5 stages: Build → Unit Test → Docker Build + Push → Deploy to Staging → Manual Gate → Deploy to Production. The same pattern I run across 12 microservices at my current company. No tutorial shortcuts.
Prerequisites
- GitHub account + a repository with a Node.js/Python/any app
- Docker Hub or AWS ECR account (for image push)
- An EC2 instance or server to deploy to (or just Staging = EC2, Prod = manual)
Stage 1: The Workflow File Structure
Create .github/workflows/ci-cd.yml in your repository root.
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: docker.io
IMAGE_NAME: yourusername/yourapp
EC2_HOST: ${{ secrets.EC2_HOST }}
jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm test
- name: Run linting
run: npm run lint
Stage 2: Docker Build + Security Scan
docker-build-push:
name: Docker Build & Push
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha-
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE_NAME }}:latest
format: 'sarif'
output: 'trivy-results.sarif'
exit-code: '1'
severity: 'CRITICAL,HIGH'
Stage 3: Deploy to Staging
deploy-staging:
name: Deploy to Staging
needs: docker-build-push
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to staging via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.STAGING_HOST }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ${{ env.IMAGE_NAME }}:latest
docker stop app-staging || true
docker rm app-staging || true
docker run -d \
--name app-staging \
--env-file /home/ubuntu/.env.staging \
-p 3000:3000 \
--restart unless-stopped \
${{ env.IMAGE_NAME }}:latest
docker system prune -f
Stage 4: Manual Gate + Production Deploy
deploy-production:
name: Deploy to Production
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production # This requires manual approval in GitHub Environments
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.PROD_HOST }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ${{ env.IMAGE_NAME }}:latest
docker stop app-prod || true && docker rm app-prod || true
docker run -d \
--name app-prod \
--env-file /home/ubuntu/.env.prod \
-p 80:3000 \
--restart always \
${{ env.IMAGE_NAME }}:latest
Setting Up GitHub Environments for Manual Approval
Go to your repo → Settings → Environments → Create "production" → Add required reviewers. When a deploy hits this stage, GitHub will pause and wait for a human to approve. This is the manual gate that prevents accidental production deployments.
Secrets to Configure
DOCKERHUB_USERNAMEandDOCKERHUB_TOKENSSH_PRIVATE_KEY— the private key to SSH into your serversSTAGING_HOSTandPROD_HOST— IP addresses of your servers
/health endpoint and fail the pipeline if it returns anything other than 200. Silent failed deploys are the worst kind.
Ready to stop learning theory and start building real projects? Join the Cloudshalla masterclasses to get 1-on-1 mentorship, break into top-tier DevOps roles, and master cloud automation today.