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

Cloud Engineering Architecture

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_USERNAME and DOCKERHUB_TOKEN
  • SSH_PRIVATE_KEY — the private key to SSH into your servers
  • STAGING_HOST and PROD_HOST — IP addresses of your servers
💡 Pro tip from production: Always add a smoke test job after each deployment — hit a /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.