The image is off the local machine. GitHub builds it now.
The Minikube manual build loop is gone. A GitHub Actions workflow builds the Docker image on ubuntu-latest, tags it with :latest and :sha-<commit>, and pushes both to ghcr.io. The GHA layer cache skips the PyTorch layer on every run after the first. deployment.yaml points to ghcr.io. Two pods confirmed running the remote image.
Every update to the model API required building the image inside Minikube’s daemon, waiting, and loading it manually. That loop is gone.
The constraint
The deployment had image: simple-model-api:latest and imagePullPolicy: IfNotPresent. That tag only existed inside Minikube’s Docker daemon. Updating the running service meant:
- Pointing the Docker client at Minikube’s internal daemon with
eval $(minikube docker-env). - Running a full image build through the VM’s NAT bridge — the same path that hung for over an hour in transmission 010 because pip was downloading PyTorch through it.
- Running
kubectl rollout restartand waiting three minutes for the ResNet-50 model to load before the pods turned ready.
There was no automation, no audit trail, and no way to recover a specific version of the image after the fact. Every deploy was manual and unrepeatable.
The proof
The workflow
.github/workflows/ci-cd.yml triggers on every push to main. It runs on GitHub’s ubuntu-latest runner.
on:
push:
branches:
- main
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
GITHUB_TOKEN is injected automatically by GitHub Actions. The permissions block grants it write access to ghcr.io. No manual secrets are needed.
Two tags are pushed per commit:
tags: |
ghcr.io/mohamed-shaaker/simple-model-api:latest
ghcr.io/mohamed-shaaker/simple-model-api:${{ steps.vars.outputs.sha_short }}
:latest is what the deployment tracks. :sha-<commit> is the immutable rollback handle — kubectl set image to any past SHA is a valid recovery path without touching the manifest.
The GHA layer cache stores the Docker build layers between runs:
cache-from: type=gha
cache-to: type=gha,mode=max
The first build is slow — it downloads PyTorch and bakes the ResNet-50 weights into the image. Every run after that skips those layers. The ~2 GB download does not happen again.
The deployment change
Two lines changed in kubernetes/deployment.yaml:
# before
image: simple-model-api:latest
imagePullPolicy: IfNotPresent
# after
image: ghcr.io/mohamed-shaaker/simple-model-api:latest
imagePullPolicy: Always
Always is required. Without it, kubectl rollout restart does not fetch the new digest — it reuses whatever is cached on the node and the pods come up running old code. That is the exact stale-image problem that produced the /metrics 404 in transmission 010.
imagePullSecrets was added under spec for private package access:
imagePullSecrets:
- name: ghcr-credentials
The secret is created once with:
kubectl create secret docker-registry ghcr-credentials \
--docker-server=ghcr.io \
--docker-username=Mohamed-shaaker \
--docker-password=YOUR_GITHUB_PAT
If the GHCR package is set to Public, this block is unnecessary.
The pods
NAME READY STATUS RESTARTS AGE
simple-model-api-deployment-677d4f69fd-2dmj2 1/1 Running 0 8m51s
simple-model-api-deployment-677d4f69fd-2gqw5 1/1 Running 0 16m
Both pods healthy. I confirmed the running image directly from the deployment spec:
$ kubectl get deployment simple-model-api-deployment \
-o jsonpath='{.spec.template.spec.containers[0].image}'
ghcr.io/mohamed-shaaker/simple-model-api:latest
The local Minikube daemon is no longer in the loop.
Where this sits
| Item | Status |
|---|---|
GitHub Actions workflow — push to main triggers build | Done |
ghcr.io push — :latest and :sha-<commit> per commit | Done |
| GHA layer cache — PyTorch layer skipped after first run | Done |
deployment.yaml — image updated to ghcr.io | Done |
imagePullPolicy — IfNotPresent replaced with Always | Done |
imagePullSecrets — ghcr-credentials block added | Done |
| Both pods confirmed running the remote image | Done |
| GHCR package visibility — public vs. private | Open |
Future deploys are git push. The rollout is kubectl rollout restart.