What is CI/CD and Why Does It Matter?

Continuous Integration and Continuous Delivery (CI/CD) is the backbone of modern software delivery. Instead of manually building, testing, and deploying code, a CI/CD pipeline automates the entire process — from the moment a developer pushes a commit to the point where it reaches production.

In this guide, we'll build a real-world pipeline using GitHub Actions, Docker, and a simple Node.js application. By the end, you'll have a fully automated workflow that tests, builds, and deploys your app on every push.

The Three Pillars of a Good Pipeline

Before writing a single line of YAML, understand what your pipeline needs to do:

  • Build — compile your code and create a Docker image
  • Test — run unit tests, integration tests, and security scans
  • Deploy — push the image to a registry and update your infrastructure

Step 1: Setting Up GitHub Actions

Create a .github/workflows/pipeline.yml file in your repository:

name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

Step 2: Writing a Production-Ready Dockerfile

A multi-stage Dockerfile keeps your final image lean and secure:

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]

Step 3: Automated Deployments to AWS

Add a deploy job that only runs on pushes to main:

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-central-1

      - name: Push to ECR and deploy
        run: |
          aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REGISTRY
          docker tag myapp:${{ github.sha }} $ECR_REGISTRY/myapp:latest
          docker push $ECR_REGISTRY/myapp:latest
          aws ecs update-service --cluster production --service myapp --force-new-deployment

Common Pitfalls to Avoid

After setting up dozens of pipelines, here are the mistakes I see most often:

  • Storing secrets in the repo — always use GitHub Secrets or a secrets manager like AWS Secrets Manager
  • Skipping tests on feature branches — test everything, every time
  • Fat Docker images — use multi-stage builds and Alpine base images
  • No rollback strategy — always keep the previous image tagged and ready

Next Steps

Once your basic pipeline is running, consider adding:

  • Security scanning with Trivy or Snyk
  • Code quality gates with SonarQube
  • Notifications to Slack on deployment success or failure
  • Blue/green deployments to eliminate downtime

A solid CI/CD pipeline is the single biggest productivity boost you can give your team. Start simple, then layer on complexity as your needs grow.