Kubernetes itself doesn’t actually care how you get your container images into its registry or how you tell it to run them; it just wants to know that the image is there and what your desired state is. GitHub Actions is just one tool among many that can automate that process for you.

Let’s see it in action.

Imagine you’ve got a simple Nginx deployment. Your deployment.yaml might look like this:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: your-dockerhub-username/my-nginx:latest # This is what we'll update
        ports:
        - containerPort: 80

And your service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: my-nginx-service
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer # Or ClusterIP, NodePort depending on your setup

Now, you want to build this Nginx image whenever you push to your main branch, push it to Docker Hub, and then update your Kubernetes deployment to use the new image. Here’s a GitHub Actions workflow that does just that:

name: Deploy to Kubernetes

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Log in to Docker Hub
      uses: docker/login-action@v2
      with:

        username: ${{ secrets.DOCKERHUB_USERNAME }}


        password: ${{ secrets.DOCKERHUB_TOKEN }}


    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true

        tags: your-dockerhub-username/my-nginx:${{ github.sha }} # Tag with commit SHA

        cache-from: type=registry,ref=your-dockerhub-username/my-nginx:buildcache
        cache-to: type=registry,ref=your-dockerhub-username/my-nginx:buildcache

    - name: Set up Kubeconfig
      uses: azure/k8s-set-context@v3
      with:
        method: kubeconfig

        kubeconfig: ${{ secrets.KUBECONFIG_DATA }}


    - name: Deploy to Kubernetes
      run: |
        kubectl apply -f deployment.yaml

        kubectl set image deployment/my-nginx-deployment nginx=your-dockerhub-username/my-nginx:${{ github.sha }} --record

Let’s break down what’s happening:

  1. on: push: branches: - main: This triggers the workflow every time code is pushed to the main branch.

  2. actions/checkout@v3: This action checks out your repository’s code so the workflow can access your Dockerfile and Kubernetes manifests.

  3. docker/setup-buildx-action@v2: This sets up Docker Buildx, a multi-platform builder that allows for more efficient Docker image building.

  4. docker/login-action@v2: This step securely logs you into your Docker Hub account using secrets you’ve stored in GitHub.

  5. docker/build-push-action@v4: This is where the magic happens. It builds your Docker image using the Dockerfile in your repository and pushes it to Docker Hub. Crucially, it tags the image with the Git commit SHA (${{ github.sha }}). This ensures that each deployment is tied to a specific version of your code. We also use build cache to speed up subsequent builds.

  6. azure/k8s-set-context@v3: This action configures kubectl to connect to your Kubernetes cluster. You’ll need to store your cluster’s kubeconfig file as a GitHub secret named KUBECONFIG_DATA.

  7. kubectl apply -f deployment.yaml: This applies your deployment manifest. If the deployment doesn’t exist, it creates it. If it does, it will ensure the desired state is maintained.

  8. kubectl set image deployment/my-nginx-deployment nginx=your-dockerhub-username/my-nginx:${{ github.sha }} --record: This is the core deployment command. It tells Kubernetes to update the my-nginx-deployment to use the new image we just built and pushed. The nginx= part specifies which container within the deployment needs to be updated (matching the name in your deployment.yaml). The --record flag is useful for auditing changes.

The actual "deployment" in Kubernetes is an ongoing process. When you run kubectl set image, Kubernetes doesn’t instantly swap out all your running containers. Instead, it initiates a rolling update. It will gradually terminate old pods and start new ones with the updated image, ensuring zero downtime if your application is designed for it. The Deployment object in Kubernetes manages this entire lifecycle, handling the desired number of replicas, update strategy, and rollbacks.

What most people miss is that the image: your-dockerhub-username/my-nginx:latest in your deployment.yaml is often a red herring in automated deployments. You never want to hardcode :latest in a production deployment. Using the commit SHA as the tag (your-dockerhub-username/my-nginx:${{ github.sha }}) is the standard practice because it provides immutability and traceability. Each commit gets a unique image tag, and your Kubernetes deployment then points to that specific, immutable image.

The next step you’ll likely encounter is managing Kubernetes secrets and configurations dynamically within your CI/CD pipeline, rather than relying on static YAML files.

Want structured learning?

Take the full Github course →