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
--setor 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
| Problem | Root Cause | Fix |
|---|---|---|
| ImagePullBackOff after deploy | Registry auth not set on cluster | Create imagePullSecret and add to serviceAccount |
| Deploy succeeds, app crashes | No health check in pipeline | Add rollout status --timeout step |
| Secrets visible in logs | Using echo to debug | Use ::add-mask:: in Actions or avoid printing secrets |
Stale latest tag pulled | imagePullPolicy: IfNotPresent | Use SHA tags and imagePullPolicy: Always |
| Pipeline hangs | --wait flag with broken readiness probe | Fix 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.