You built api:dev, ran kind load docker-image api:dev, and the pod still shows ErrImagePull: failed to pull and unpack image "docker.io/library/api:dev": pull access denied — or it starts fine but keeps running yesterday's code even after a fresh load. kind load is the simplest way to get a locally built image into a kind cluster, with no registry to run, but it interacts with pull policies and tags in ways that are not obvious. This page covers the three failures that make it seem broken, as part of local Kubernetes development.

For small images and single-node clusters, loading directly is often the better choice than running a registry: nothing extra to start, nothing to wire into containerd, and no network hop.

Diagnostic

Compare what is loaded into the node with what the pod asks for and how it asks:

#!/usr/bin/env bash
set -euo pipefail
node="shop-control-plane"
docker exec "$node" crictl images | grep -E 'IMAGE|api' || true
kubectl --context kind-shop get deploy api -o jsonpath='{.spec.template.spec.containers[0].image}{"  pullPolicy="}{.spec.template.spec.containers[0].imagePullPolicy}{"\n"}'
kubectl --context kind-shop get pods -l app=api -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.status.containerStatuses[0].imageID}{"\n"}{end}'
docker image inspect api:dev --format 'local id={{.Id}} arch={{.Architecture}}'

Expected bad output for the pull-policy case:

IMAGE                      TAG     IMAGE ID        SIZE
docker.io/library/api      dev     4b1e9c7a2d11    61.2MB
api:dev  pullPolicy=Always
api-5d8f7c9b4-k2m9x
local id=sha256:4b1e9c7a2d11... arch=arm64

The image is present on the node, but imagePullPolicy: Always makes the kubelet contact Docker Hub for docker.io/library/api:dev, which does not exist, so the pod never uses the loaded copy.

Why the Loaded Image Is Not Used Decision diagram mapping three symptoms after kind load to their causes. Why the Loaded Image Is Not Used What does the pod show? ErrImagePull pull policy Always or tag latest Running old code same tag, spec unchanged exec format error image arch differs from node
Pull policy, tag reuse and architecture account for nearly every kind load failure.

Root cause

kind load docker-image exports the image from the host's Docker store and imports it into each node's containerd store. The kubelet then decides whether to pull based on imagePullPolicy: with Always, it contacts the registry every time a container starts and fails when the image exists only locally; with IfNotPresent, it uses the local copy. If the tag is latest or omitted, Kubernetes defaults the policy to Always, which is why api:latest fails where api:dev works. The stale-code case comes from Kubernetes' change detection: pods are only replaced when the pod template changes. Loading a new image under the same tag changes nothing in the Deployment, so existing pods keep running the old image, and new pods may even start from the old cached layer if the load raced with scheduling. The architecture case is simpler: an image built for linux/amd64 on an Intel CI runner or with --platform cannot run on an arm64 kind node, and the container exits immediately with exec format error.

Each cause has a precise fix, and none requires a registry.

Resolution

  1. Use a specific tag and IfNotPresent. Never use latest for locally loaded images:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: shop/api:dev
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
  1. Give every build a unique tag and update the Deployment to it, so Kubernetes sees a change and rolls out:
#!/usr/bin/env bash
set -euo pipefail
tag="dev-$(git rev-parse --short HEAD)-$(date +%s)"
docker build -t "shop/api:$tag" ./api
kind load docker-image "shop/api:$tag" --name shop
kubectl --context kind-shop set image deployment/api api="shop/api:$tag"
kubectl --context kind-shop rollout status deployment/api --timeout=90s

If you must keep a fixed tag — for example because a Helm values file references it — force a rollout after the load with kubectl rollout restart deployment/api, which changes a template annotation.

  1. Build for the node's architecture. Check the node and build to match:
#!/usr/bin/env bash
set -euo pipefail
node_arch=$(kubectl --context kind-shop get nodes -o jsonpath='{.items[0].status.nodeInfo.architecture}')
docker buildx build --platform "linux/$node_arch" --load -t shop/api:dev ./api
docker image inspect shop/api:dev --format '{{.Architecture}}'
  1. Clean up old loaded images occasionally. Every unique tag stays in the node's containerd store until removed:
#!/usr/bin/env bash
set -euo pipefail
for node in $(kind get nodes --name shop); do
  docker exec "$node" crictl rmi --prune
done

crictl rmi --prune removes images not used by any running container, which is safe for a development cluster.

A Reliable kind load Cycle Flow from a uniquely tagged build through kind load to a rollout that picks up the new image. A Reliable kind load Cycle build unique tag, node arch kind load copy into nodes set image template changes rollout new pods, new code
The unique tag is what turns a load into a rollout.

Expected output

$ kubectl --context kind-shop rollout status deployment/api --timeout=90s
deployment "api" successfully rolled out
$ kubectl --context kind-shop get pods -l app=api -o jsonpath='{.items[0].spec.containers[0].image}{"\n"}'
shop/api:dev-3f2a91c-1726651200
$ kubectl --context kind-shop logs deploy/api | head -1
server listening on :8080 (build 3f2a91c)

The pod runs the freshly loaded image, identified by its unique tag, and logs the commit it was built from.

Prevention

  1. Log the build commit at startup. Passing --build-arg GIT_SHA=$(git rev-parse --short HEAD) into the image and printing it on boot makes "is this pod running my code?" a one-line check.

  2. Lint manifests for latest. Fail CI when a local values file uses :latest or omits the tag; the policy default it triggers is the most common reason loaded images are ignored.

  3. Switch to a registry when loads get slow. Loading copies the whole image into every node each time. Past roughly 500 MB or two nodes, a local registry is faster because only changed layers move.

kind load Time by Image Size Bar chart of kind load duration for three image sizes on a single-node cluster. kind load Time by Image Size 120 MB image 4 s 600 MB image 19 s 1.4 GB image 47 s
Load time grows with the full image size, not the size of the change.

Platform caveats

Apple Silicon (ARM64): kind nodes are arm64. Images pulled from CI artifacts built for amd64 fail with exec format error; rebuild locally or use multi-arch images from buildx.

macOS (Docker Desktop): kind load needs the image in the classic image store. With the containerd image store enabled in Docker Desktop, multi-platform images may fail to export; build with --load for a single platform.

Podman: use kind load image-archive with a tarball from podman save, since kind load docker-image expects the Docker CLI's image store.

Rollback

Point the Deployment back at a known-good tag, which is still in the node's store unless pruned:

#!/usr/bin/env bash
set -euo pipefail
kubectl --context kind-shop rollout undo deployment/api
kubectl --context kind-shop rollout status deployment/api

Frequently Asked Questions

Why does kind try to pull an image I already loaded?

The container's imagePullPolicy is Always, explicitly or because the tag is latest. Set a specific tag and imagePullPolicy: IfNotPresent so the kubelet uses the copy already in the node's containerd store.

Why are my pods still running old code after kind load?

Kubernetes replaces pods only when the pod template changes. Loading a new image under the same tag changes nothing, so use a unique tag per build and kubectl set image, or run kubectl rollout restart after loading.

Does kind load work with multi-node clusters?

Yes, it loads into every node by default, which is also why it gets slow: the full image is copied once per node. Use --nodes to target specific nodes, or a local registry for large images.

How do I see which images are loaded into a node?

Run docker exec <node-name> crictl images. kind nodes are containers, so docker exec gives direct access to containerd's CLI on each node.