Skip to content

CI/CD with Docker and Kubernetes — Complete Pipeline

DodaTech 3 min read

In this tutorial, you'll learn about CI/CD with Docker and Kubernetes. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

What You'll Learn

Build a complete CI/CD pipeline — automatically build Docker images, run tests, push to a registry, and deploy to Kubernetes on every git push.

Why It Matters

Manual deployments are error-prone and slow. A CI/CD pipeline ensures consistent, repeatable, and automated delivery of your applications.

Real-World Use

Every push to main deploys to staging automatically, a tag triggers production rollout, or a Pull Request creates a preview environment.

Pipeline Overview

Developer pushes code
    ↓
CI/CD (GitHub Actions)
    ├── 1. Checkout code
    ├── 2. Run tests
    ├── 3. Build Docker image
    ├── 4. Push to registry
    └── 5. Deploy to Kubernetes
           ↓
Production cluster

Dockerfile

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

GitHub Actions Pipeline

# .github/workflows/deploy.yml
name: Build and Deploy

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

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  K8S_NAMESPACE: production

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
      - run: npm run build

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker 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

      - name: Deploy to Kubernetes
        run: |
          # Update the image tag in the deployment
          sed -i "s|image:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}|" k8s/deployment.yaml

          # Apply to cluster
          kubectl apply -f k8s/
          kubectl rollout status deployment/my-app \
            -n ${{ env.K8S_NAMESPACE }}
        env:
          KUBECONFIG: ${{ secrets.KUBECONFIG }}

Kubernetes Manifests

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: production
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: ghcr.io/myorg/my-app:latest
          ports:
            - containerPort: 3000
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
  namespace: production
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - myapp.example.com
      secretName: myapp-tls
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80

Environment-Specific Deployments

# k8s/overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
namespace: staging
replicas:
  - name: my-app
    count: 2
images:
  - name: ghcr.io/myorg/my-app
    newTag: staging

# k8s/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
namespace: production
replicas:
  - name: my-app
    count: 5

Rollback Strategy

# Check rollout history
kubectl rollout history deployment/my-app -n production

# Rollback to previous version
kubectl rollout undo deployment/my-app -n production

# Rollback to specific revision
kubectl rollout undo deployment/my-app -n production --to-revision=3

Complete Flow

1. Developer pushes code to main
2. GitHub Actions triggers
3. Tests run (unit + integration)
4. Docker image built with Git SHA tag
5. Image pushed to GitHub Container Registry
6. k8s/deployment.yaml updated with new tag
7. kubectl apply updates the cluster
8. Rolling update gradually replaces pods
9. Health checks verify new pods are healthy
10. Old pods are terminated

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro