Skip to main content
K8sCalc
kubernetes27 May 2026

CI/CD for Kubernetes with GitHub Actions: A Complete Guide (2026)

A practical walkthrough of building a full GitHub Actions pipeline that builds a container image, pushes it to a registry, and deploys to Kubernetes — with secrets handling, rollback, and Helm support.

Every Kubernetes team eventually needs a repeatable, automated path from a git push to a running container in the cluster. GitHub Actions is the default choice in 2026 — it's free for public repos, deeply integrated with container registries, and handles secrets natively without a separate secrets manager.

This guide covers the full pipeline: build, push, deploy, secrets, and rollback. Use the GitHub Actions K8s Deploy Generator to scaffold the workflow file for your specific setup, then read below to understand each stage.

Pipeline Architecture

Developer pushes to main
        │
        ▼
GitHub Actions Runner
  1. Checkout code
  2. Build Docker image
  3. Push to container registry (GHCR / DockerHub / ECR)
  4. Deploy to Kubernetes
       ├── kubectl apply  (simple deployments)
       └── helm upgrade   (Helm-based workloads)
        │
        ▼
Kubernetes cluster pulls new image
Rolling update → Pods replaced zero-downtime

Step 1: Building and Pushing the Container Image

The first job builds your image and pushes it to a registry. Using GitHub Container Registry (GHCR) keeps everything in one place and uses your existing GitHub token for auth.

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

on:
  push:
    branches: [main]

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

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Extract Docker metadata
        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' }}

      - name: Build and push image
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Tag images with the Git SHA, not latest. A latest tag in Kubernetes is a footgun — it makes rollbacks ambiguous and breaks reproducibility. The SHA tag is immutable and ties a running pod directly to a specific commit.

Step 2: Deploy with kubectl apply

For teams without Helm, kubectl apply is the straightforward approach. Pass your kubeconfig as a secret and update the image tag before applying:

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure kubectl
        uses: azure/setup-kubectl@v4

      - name: Set kubeconfig
        run: |
          mkdir -p ~/.kube
          echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
          chmod 600 ~/.kube/config

      - name: Update deployment image
        run: |
          IMAGE_TAG="ghcr.io/${{ github.repository }}:sha-${{ github.sha }}"
          kubectl set image deployment/my-app \
            app=${IMAGE_TAG} \
            --namespace production

      - name: Wait for rollout
        run: kubectl rollout status deployment/my-app --namespace production --timeout=120s

kubectl rollout status is essential — it blocks the job until the rollout completes (or fails), so you know immediately if the deploy broke something.

Step 3: Deploy with Helm

For teams using Helm, helm upgrade --install is idempotent and handles first installs cleanly. Use the Helm Chart Values Generator to generate a production-ready values.yaml, then override the image tag at deploy time:

      - name: Deploy with Helm
        run: |
          helm upgrade --install my-app ./charts/my-app \
            --namespace production \
            --create-namespace \
            --set image.repository=ghcr.io/${{ github.repository }} \
            --set image.tag=sha-${{ github.sha }} \
            --set replicaCount=3 \
            --wait \
            --timeout 120s \
            --atomic

The --atomic flag is critical: if the upgrade fails, Helm automatically rolls back to the previous release. Without it, a failed deploy leaves the chart in a broken state.

Secrets Handling

Never hardcode credentials in your workflow YAML. GitHub Secrets are the right tool for:

  • KUBECONFIG — base64-encoded kubeconfig for cluster access
  • DOCKER_PASSWORD — for external registries (not needed for GHCR)
  • Application secrets passed via --set or Helm values

For the kubeconfig, create a service account with minimal permissions rather than copying the cluster admin kubeconfig:

# Create a dedicated CI service account
kubectl create serviceaccount github-actions -n production

# Bind only the permissions needed for deployments
kubectl create rolebinding github-actions-deploy \
  --role=deploy-role \
  --serviceaccount=production:github-actions \
  --namespace=production

# Get the token and base64-encode it for GitHub Secrets
kubectl create token github-actions -n production --duration=87600h | base64 -w 0

For application secrets (database passwords, API keys), use sealed-secrets or an external secrets operator to keep them out of GitHub entirely.

Rollback Strategy

When a deploy fails, you need to roll back fast. There are two approaches depending on your deploy method:

kubectl rollback:

# Roll back to the previous revision
kubectl rollout undo deployment/my-app --namespace production

# Roll back to a specific revision
kubectl rollout undo deployment/my-app --to-revision=5 --namespace production

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

Helm rollback:

# List Helm release history
helm history my-app --namespace production

# Roll back to the previous release
helm rollback my-app --namespace production

# Roll back to a specific revision
helm rollback my-app 3 --namespace production

For automated rollback in the pipeline, use the --atomic flag with Helm (as shown above) or add a rollback step triggered on job failure:

      - name: Rollback on failure
        if: failure()
        run: |
          kubectl rollout undo deployment/my-app --namespace production
          kubectl rollout status deployment/my-app --namespace production

Environment Promotion

Structure your workflow to deploy to staging first, then require manual approval for production:

  deploy-staging:
    needs: build-and-push
    environment: staging
    # ... deploy steps

  deploy-production:
    needs: deploy-staging
    environment: production  # GitHub environment with required reviewers
    # ... deploy steps

The environment: production combined with branch protection rules means a human must approve the production deploy in the GitHub UI before it runs.

Common Pitfalls

ProblemRoot CauseFix
ImagePullBackOff after deployRegistry auth not set on clusterCreate imagePullSecret and add to serviceAccount
Deploy succeeds, app crashesNo health check in pipelineAdd rollout status --timeout step
Secrets visible in logsUsing echo to debugUse ::add-mask:: in Actions or avoid printing secrets
Stale latest tag pulledimagePullPolicy: IfNotPresentUse SHA tags and imagePullPolicy: Always
Pipeline hangs--wait flag with broken readiness probeFix probe or add --timeout to Helm command

A working CI/CD pipeline for Kubernetes isn't complicated, but the details matter. The scaffolding from the GitHub Actions K8s Deploy Generator gets you to a working baseline in minutes — the concepts above make sure you understand what each piece does when things go wrong.