CI/CD Pipeline Design Guide 2026 — From Commit to Production in Under 15 Minutes
Advertisement
Introduction
Why This Matters
A well-designed CI/CD pipeline takes you from code commit to production in under 15 minutes with zero downtime. Teams with mature pipelines deploy multiple times per day while maintaining higher reliability than teams that deploy monthly. This guide builds a complete pipeline covering quality gates, Docker builds, staged deployments, blue-green releases, and rollback strategies.
Pipeline Architecture
Commit
├── Lint + Type Check (parallel)
├── Unit Tests (parallel)
└── Security Scan (parallel)
↓ (all pass)
Docker Build + Push
↓
Deploy to Staging
↓
E2E Tests on Staging
↓ (manual approval optional)
Blue-Green Deploy to Production
↓
Smoke Tests
↓
Slack NotificationComplete Pipeline with GitHub Actions
# .github/workflows/pipeline.yml
name: Production Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
name: Code Quality
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
- run: npm run typecheck
- run: npm run format:check
test:
name: Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 3s
--health-retries 5
ports: ['5432:5432']
redis:
image: redis:7-alpine
ports: ['6379:6379']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm run db:migrate
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb
- run: npm run test:ci
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: [quality, test]
outputs:
image: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha-
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- 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
platforms: linux/amd64,linux/arm64
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
environment:
name: staging
url: https://staging.myapp.com
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- 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: us-east-1
- name: Deploy to staging ECS
run: |
aws ecs update-service \
--cluster staging \
--service myapp-staging \
--force-new-deployment
aws ecs wait services-stable \
--cluster staging \
--services myapp-staging
e2e:
name: E2E Tests on Staging
runs-on: ubuntu-latest
needs: deploy-staging
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
env:
BASE_URL: https://staging.myapp.com
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: e2e
environment:
name: production
url: https://myapp.com
steps:
- uses: actions/checkout@v4
- name: Blue-green deploy
run: |
kubectl apply -f k8s/production/
kubectl rollout status deployment/myapp-production --timeout=5mBlue-Green Deployment
# Switch traffic from blue (old) to green (new) with zero downtime
apiVersion: v1
kind: Service
metadata:
name: myapp-production
spec:
selector:
app: myapp
slot: blue # Switch to 'green' to flip traffic
---
# Deploy green, verify, then flip the service
# kubectl apply -f k8s/deployment-green.yaml
# kubectl rollout status deployment/myapp-green
# kubectl patch service myapp-production \
# -p '{"spec":{"selector":{"slot":"green"}}}'
#
# Rollback: flip back to blue
# kubectl patch service myapp-production \
# -p '{"spec":{"selector":{"slot":"blue"}}}'Canary Releases
# Send 10% of traffic to the new version with nginx-ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: myapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-canary-service
port:
number: 80Rollback Strategy
#!/bin/bash
# emergency-rollback.sh
set -euo pipefail
DEPLOYMENT="${1:-myapp-production}"
NAMESPACE="${2:-production}"
echo "Rolling back $DEPLOYMENT in $NAMESPACE..."
kubectl rollout undo "deployment/$DEPLOYMENT" -n "$NAMESPACE"
kubectl rollout status "deployment/$DEPLOYMENT" -n "$NAMESPACE" --timeout=3m
echo "Rollback complete. New active image:"
kubectl get deployment "$DEPLOYMENT" -n "$NAMESPACE" \
-o jsonpath='{.spec.template.spec.containers[0].image}'Deployment Notifications
notify:
name: Notify Team
needs: [deploy-production]
runs-on: ubuntu-latest
if: always()
steps:
- name: Slack notification
uses: slackapi/slack-github-action@v1
with:
channel-id: '#deployments'
slack-message: |
*Deployment ${{ needs.deploy-production.result == 'success' && 'Succeeded' || 'Failed' }}*
Version: `${{ github.sha }}`
Branch: `${{ github.ref_name }}`
By: ${{ github.actor }}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}Common Mistakes
- No
concurrencygroups — without them, multiple pushes to main trigger parallel deployments that race and corrupt state - Missing
needs:between jobs — without it, deploy runs in parallel with tests and may deploy broken code - Single-environment pipelines — deploying directly to production without a staging environment removes your safety net
- No rollback plan — every deployment needs a documented and tested rollback path before it is approved
- Not caching Docker layers — without
cache-from: type=gha, Docker rebuilds from scratch on every run
Best Practices
- Run quality checks (lint, types) in parallel with tests to minimize total pipeline time
- Use GitHub Environments with required reviewers for production deploys to enforce human approval
- Always build the Docker image once and promote the same image through staging to production
- Track DORA metrics (deployment frequency, lead time, failure rate, recovery time) to measure pipeline health
- Set
timeout-minuteson every job to prevent runaway pipelines from consuming CI minutes
Key Takeaways
- Parallel jobs for lint, tests, and security scanning minimize total pipeline time without cutting corners
concurrencygroups cancel in-progress runs on the same branch, preventing stale deploys from racing with newer ones- Build Docker images once with a git SHA tag and promote the same artifact from staging to production
- Blue-green deployments achieve zero-downtime by running two identical environments and switching traffic at the load balancer
- Canary releases let you validate a new version on 5-10% of traffic before committing to a full rollout
- DORA metrics are the industry standard for measuring pipeline maturity: deployment frequency, lead time, change failure rate, and MTTR
- GitHub Environments with protection rules enforce approval gates before sensitive deploys execute
- Emergency rollback with
kubectl rollout undoreverts to the previous ReplicaSet in under 30 seconds
Advertisement
Related reading
GitHub Actions — Complete CI/CD Guide 20266 min readGitHub Actions for Docker — Build, Tag, and Push Images 20265 min readAI Tools for DevOps — Generate Dockerfiles, CI/CD Pipelines, and Kubernetes Manifests5 min readGitHub Actions vs Jenkins vs GitLab CI — CI/CD Comparison 20267 min readGitHub Actions for Node.js — Complete CI/CD Pipeline Guide 20265 min readGitLab CI/CD — Complete Pipeline Guide for 20265 min read