How to do a canary deploy on Kubernetes with GitHub Actions: step by step
This tutorial shows how to do a canary deploy on Kubernetes with GitHub Actions to roll out new versions gradually and reduce risk. I explain why canary deployments are useful; then I detail a concrete flow with Kubernetes manifests, GitHub Actions and simple verification.
Prerequisites
- GitHub account and a repository with the application code (e.g., public or private Docker image).
- Accessible Kubernetes cluster (minikube, kind or a cloud cluster) with kubectl configured.
- GitHub Actions runner with permissions for kubectl (use KUBECONFIG secrets or a token).
- Basic Kubernetes knowledge: Deployment, Service and labels.
Step 1: Prepare Kubernetes manifests to support canary
Create a primary Deployment and prepare a separate Deployment for the canary. The idea is to have two Deployments (app-primary, app-canary) and a Service that balances by label. This way we can control traffic to the canary by changing replicas or weights in the Ingress/Service.
# deployment-primary.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-primary
spec:
replicas: 3
selector:
matchLabels:
app: myapp
role: primary
template:
metadata:
labels:
app: myapp
role: primary
spec:
containers:
- name: myapp
image: myrepo/myapp:stable
ports:
- containerPort: 80
# deployment-canary.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-canary
spec:
replicas: 1
selector:
matchLabels:
app: myapp
role: canary
template:
metadata:
labels:
app: myapp
role: canary
spec:
containers:
- name: myapp
image: myrepo/myapp:canary
ports:
- containerPort: 80
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-svc
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 80
type: ClusterIP
Step 2: Configure GitHub Actions to build image and perform canary deploy
Create a workflow that builds the canary image, pushes it to the registry and applies the manifests to the cluster. Use a job that updates only the canary Deployment to the new tag; that way the primary keeps the stable version.
# .github/workflows/canary-deploy.yaml
name: Canary Deploy
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push image
uses: docker/build-push-action@v4
with:
push: true
tags: myrepo/myapp:${{ github.sha }}
# configure registry credentials via secrets
- name: Configure kubectl
run: |
echo "$KUBECONFIG" > kubeconfig
export KUBECONFIG=$PWD/kubeconfig
env:
KUBECONFIG: ${{ secrets.KUBECONFIG }}
- name: Update canary deployment image
run: |
kubectl set image deployment/app-canary myapp=myrepo/myapp:${{ github.sha }}
kubectl rollout status deployment/app-canary --timeout=60s
Step 3: Gradually increase traffic to the canary
There are several ways to increase traffic: increase the canary replicas, use Ingress/Service with weights (e.g., Istio, Traefik) or manipulate selectors. Here we show the simple approach: increase the canary replicas and later reduce the primary's.
# scale-canary.sh (run via GitHub Action or manually)
kubectl scale deployment/app-canary --replicas=2
# after verification
kubectl scale deployment/app-primary --replicas=2
# finally promote the canary to primary by changing images and restoring replicas
kubectl set image deployment/app-primary myapp=myrepo/myapp:${CANARY_TAG}
kubectl scale deployment/app-canary --replicas=0
kubectl scale deployment/app-primary --replicas=3
Step 4: Monitoring and automatic rollback
Configure liveness/readiness health checks and use a simple script to check metrics (errors, latency) or the readiness state. In the example below, a workflow step checks endpoints and rolls back if the error rate increases.
# simplified example of verification via curl
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://myapp-svc/health)
if [ "$STATUS" != "200" ]; then
kubectl set image deployment/app-canary myapp=myrepo/myapp:stable
kubectl rollout status deployment/app-canary --timeout=60s
exit 1
fi
Verify the result
Confirm that the canary is running the new image and serving traffic only to a subset of users. Use kubectl to inspect:
kubectl get deployments
kubectl get pods -l role=canary -o wide
kubectl describe deployment app-canary
kubectl logs deployment/app-canary --tail=50
If you use an Ingress or a weighted Service, check metrics in the Ingress controller or observability tools (Prometheus/Grafana). Test endpoints and confirm that rollback works by simulating failures.
Conclusion
Canary deploys on Kubernetes with GitHub Actions allow reducing release risk by controlling traffic gradually. Next steps: integrate Istio/Linkerd for traffic shifting by weight, automate metrics with Prometheus and create automatic gates in the workflow. Tip: start with replicas and simple health checks before introducing a service mesh — what is the biggest concern you want to mitigate with canaries?