Transmission 021 · 2026-08-19

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:

  1. Pointing the Docker client at Minikube’s internal daemon with eval $(minikube docker-env).
  2. 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.
  3. Running kubectl rollout restart and 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

ItemStatus
GitHub Actions workflow — push to main triggers buildDone
ghcr.io push — :latest and :sha-<commit> per commitDone
GHA layer cache — PyTorch layer skipped after first runDone
deployment.yaml — image updated to ghcr.ioDone
imagePullPolicyIfNotPresent replaced with AlwaysDone
imagePullSecretsghcr-credentials block addedDone
Both pods confirmed running the remote imageDone
GHCR package visibility — public vs. privateOpen

Future deploys are git push. The rollout is kubectl rollout restart.