A CI/CD pipeline automates the process of building, testing, and deploying application code from a developer's machine to production. Continuous Integration (CI) automatically builds and tests every code change. Continuous Deployment (CD) automatically deploys validated changes to production. Together, they reduce deployment risk, accelerate feedback loops, and eliminate manual release processes.
The 2025 DORA State of DevOps report found that elite-performing teams deploy 973 times more frequently than low performers, with 6,570 times faster lead time from commit to production. CI/CD is the primary enabler of this performance gap.
What You Will Build
This guide walks through building a complete CI/CD pipeline with:
- Docker for containerizing the application
- GitHub Actions for CI/CD automation
- Kubernetes for production deployment
- Rolling updates with automatic rollback on failure
- Staging environment for pre-production validation
Step 1: Containerize the Application with Docker
A Dockerfile defines how your application is packaged into a container image. This example uses a multi-stage build to minimize image size.
<h1>Stage 1: Build</h1>
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
<h1>Stage 2: Production</h1>
FROM node:20-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
Key practices in this Dockerfile:
- Multi-stage build reduces image size from ~900 MB to ~150 MB by excluding build tools from the production image
- Non-root user prevents container breakout attacks (76% of containers run as root by default, per Sysdig 2024)
- HEALTHCHECK enables Kubernetes liveness probe integration
- npm ci ensures deterministic dependency installation from package-lock.json
Step 2: Write the GitHub Actions CI Workflow
Create `.github/workflows/ci.yml` to run on every push and pull request.
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
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 linter
run: npm run lint
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
if: github.event_name == 'push'
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
This workflow runs tests on every PR and only builds/pushes container images on merges to main. The GitHub Container Registry (ghcr.io) stores images for free for public repositories. Docker layer caching via GitHub Actions cache reduces build times by 50-70% on subsequent runs.
Step 3: Create Kubernetes Deployment Manifests
Define the Kubernetes resources for your application.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
labels:
app: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web-app
image: ghcr.io/your-org/your-app:latest
ports:
- containerPort: 3000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
Key deployment settings:
- maxUnavailable: 0 ensures zero downtime during rolling updates
- readinessProbe prevents routing traffic to pods that are not ready
- livenessProbe restarts pods that become unresponsive
- Resource requests and limits enable proper scheduling and prevent resource starvation
Step 4: Add the CD Workflow for Kubernetes Deployment
Extend the GitHub Actions workflow with a deployment job.
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- name: Deploy to staging
run: |
kubectl set image deployment/web-app \
web-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-n staging
kubectl rollout status deployment/web-app -n staging --timeout=300s
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBE_CONFIG_PROD }}
- name: Deploy to production
run: |
kubectl set image deployment/web-app \
web-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-n production
kubectl rollout status deployment/web-app -n production --timeout=300s
- name: Verify deployment
run: |
kubectl get pods -n production -l app=web-app
curl -sf https://your-app.com/health || exit 1
The `environment` field enables GitHub environment protection rules: require approval before production deployments, restrict which branches can deploy, and limit deployment frequency.
Step 5: Implement Rollback Strategy
Kubernetes tracks deployment history, enabling instant rollback.
Manual rollback:
<h1>View deployment history</h1>
kubectl rollout history deployment/web-app -n production
<h1>Rollback to the previous version</h1>
kubectl rollout undo deployment/web-app -n production
<h1>Rollback to a specific revision</h1>
kubectl rollout undo deployment/web-app -n production --to-revision=3
Automated rollback can be added to the deployment workflow:
- name: Deploy with automatic rollback
run: |
kubectl set image deployment/web-app \
web-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-n production
if ! kubectl rollout status deployment/web-app -n production --timeout=300s; then
echo "Deployment failed, rolling back..."
kubectl rollout undo deployment/web-app -n production
exit 1
fi
If the rollout does not complete within 5 minutes (pods fail readiness probes), the script automatically rolls back to the previous version and fails the pipeline.
Step 6: Add Post-Deployment Monitoring
After deployment, verify application health by checking key metrics. For a complete monitoring setup, see our infrastructure monitoring guide.
Essential post-deployment checks:
- HTTP status codes -- Monitor 5xx error rate for the first 10 minutes after deployment
- Latency -- Compare p50 and p99 latency against pre-deployment baseline
- Pod restart count -- Any restart within 5 minutes indicates a crash loop
- Resource usage -- Ensure CPU and memory stay within requested limits
Pipeline Architecture Summary
| Stage | Tool | Trigger | Duration | Failure Action |
| Lint | ESLint/Prettier | Every push | 30-60 seconds | Block merge |
| Test | Jest/Vitest | Every push | 1-5 minutes | Block merge |
| Build | Docker | Merge to main | 2-5 minutes | Alert team |
| Deploy Staging | kubectl | After build | 1-3 minutes | Alert team |
| Deploy Production | kubectl | After staging (manual approval) | 1-3 minutes | Auto-rollback |
| Verify | curl + metrics | After production deploy | 5-10 minutes | Auto-rollback |
Total time from merge to verified production deployment: 10-20 minutes. The 2025 DORA report classifies this as elite-level performance. For container orchestration context, see our Kubernetes guide. For securing this pipeline, our security checklist covers CI/CD-specific security controls.
Frequently Asked Questions
How much does GitHub Actions cost?
GitHub Actions is free for public repositories (unlimited minutes). For private repositories, the free tier includes 2,000 minutes/month. Additional minutes cost $0.008/minute for Linux runners. Most small teams stay within the free tier. Self-hosted runners on your own infrastructure have no per-minute cost.
Should I use GitHub Actions or Jenkins?
GitHub Actions is recommended for teams using GitHub. It requires zero infrastructure management, integrates natively with GitHub features (environments, branch protection, CODEOWNERS), and has a vast marketplace of pre-built actions. Jenkins is better for organizations with complex, highly customized pipelines or those not using GitHub for source control.
How do I handle database migrations in CI/CD?
Run database migrations as a separate Kubernetes Job before the application deployment. The Job executes migration scripts, and the deployment only proceeds if the Job succeeds. Never run migrations and application code updates simultaneously -- the old application version must be compatible with the new schema (expand/contract migration pattern).
What is the difference between Continuous Delivery and Continuous Deployment?
Continuous Delivery means every change is automatically built, tested, and staged for production, but a human approves the final deployment. Continuous Deployment means every change that passes all automated checks is automatically deployed to production without human intervention. Most organizations start with Continuous Delivery and graduate to Continuous Deployment as confidence in their test suite grows.
How do I handle secrets in CI/CD pipelines?
Never store secrets in code or environment variables in workflow files. Use GitHub Actions encrypted secrets for CI/CD credentials. For Kubernetes, use external secret stores (HashiCorp Vault, AWS Secrets Manager) with the External Secrets Operator to inject secrets into pods. Rotate all CI/CD secrets at least quarterly.