BlogCI/CD Pipeline

🔄 Building a Complete CI/CD Pipeline

Learn how to build a professional CI/CD pipeline that takes your code from commit to production automatically.

Pipeline Overview

┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐
│  Code   │───▶│  Build  │───▶│  Test   │───▶│ Deploy  │───▶│ Monitor │
│ Commit  │    │         │    │         │    │         │    │         │
└─────────┘    └─────────┘    └─────────┘    └─────────┘    └─────────┘

Tools We’ll Use

  • GitHub - Source control
  • GitHub Actions - CI/CD automation
  • Docker - Containerization
  • Docker Hub or GHCR - Image registry
  • Kubernetes or Docker Compose - Deployment

Step 1: Repository Setup

# Initialize repository
git init
git add .
git commit -m "Initial commit"
git remote add origin git@github.com:username/project.git
git push -u origin main

Step 2: Dockerfile

# Multi-stage build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
 
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
 
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
 
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

Step 3: CI Workflow

Create .github/workflows/ci.yml:

name: CI
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm run lint
 
  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm test -- --coverage
      - uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
 
  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          tags: myapp:test
          cache-from: type=gha
          cache-to: type=gha,mode=max

Step 4: CD Workflow

Create .github/workflows/cd.yml:

name: CD
 
on:
  push:
    branches: [main]
    tags: ["v*"]
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
 
    outputs:
      image_tag: ${{ steps.meta.outputs.tags }}
 
    steps:
      - uses: actions/checkout@v4
 
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=sha
 
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
 
  deploy-staging:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: staging
 
    steps:
      - name: Deploy to Staging
        run: |
          echo "Deploying to staging..."
          # kubectl apply or docker compose commands
 
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production
    if: startsWith(github.ref, 'refs/tags/v')
 
    steps:
      - name: Deploy to Production
        run: |
          echo "Deploying to production..."

Step 5: Environment Protection

Configure environments in GitHub:

  1. Go to Settings → Environments
  2. Create staging and production
  3. Add required reviewers for production
  4. Set deployment branch rules

Pipeline Best Practices

PracticeBenefit
Fast feedbackFail early, fix quickly
Parallel jobsFaster pipeline
CachingReduced build time
Secrets managementSecurity
Environment protectionControlled deployments

Monitoring Deployments

Add health checks and notifications:

- name: Health Check
  run: |
    for i in {1..30}; do
      if curl -s https://staging.example.com/health | grep -q "ok"; then
        echo "Health check passed!"
        exit 0
      fi
      sleep 10
    done
    echo "Health check failed!"
    exit 1
 
- name: Notify Slack
  if: always()
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}

🎯 Goal: Every push should be deployable. Keep your main branch always releasable!


Published: December 15, 2024