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.

Member discussion