Sign in
← Kubernetes: debugging
6 min · easy · Lesson 1 of 12

ImagePullBackOff: the Pod can't pull the image

Why a Pod can't pull its image, where the real error text sits in describe, and what to do about it.

The most common failure in Kubernetes looks boring: the Pod is created, but the container doesn't start, and the status shows ImagePullBackOff.

NAME   READY   STATUS             RESTARTS   AGE
web    0/1     ImagePullBackOff   0          90s

Kubernetes isn't at fault here. It asked for the image and didn't get it. Below, we'll look at what happens under the hood, where the real error text lives, and how to tell a typo apart from an access or network problem by that text.

How kubelet pulls the image

kubelet image on node? pull runtime containerd, CRI-O manifest and layers registry outside the cluster start container running
The link that breaks is in the middle, but what you see is the Pod's status.

kubelet is the Kubernetes agent on every node that runs containers. Given a Pod spec with a specific image, it goes through several steps in order:

  1. Checks whether it needs to pull the image at all. This is decided by the imagePullPolicy field (more on it below) and whether the image is already on the node.
  2. If it does, it asks the container runtime (containerd, CRI-O) to pull the image.
  3. The runtime parses the image name, finds the registry host through DNS, and requests the manifest and layers from the registry.
  4. Once everything is downloaded, the runtime creates and starts the container.

Any step can fail. Based on the reason, kubelet sets its own state on the container, and you see it in the STATUS column.

The family of image errors

StatusWhat it meansTypical cause
ErrImagePullthe attempt to pull the image faileda typo in the name or tag, no access, the registry host can't be found, no network
ImagePullBackOffkubelet is waiting before the next attemptthe same thing as above: there have already been several attempts
InvalidImageNamethe image name doesn't parsecapital letters in the repository name, Cyrillic characters, extra characters
ErrImageNeverPullpulling is forbidden, and the node doesn't have the imageimagePullPolicy: Never and the image hasn't been loaded onto the node

There's essentially no difference between ErrImagePull and ImagePullBackOff. It's one error. After a failure, kubelet simply waits before the next attempt, and the container shows as ImagePullBackOff the whole time. The pause grows: 10 seconds, 20, 40, and so on, until it hits a ceiling of 5 minutes. So a Pod that's been sitting in ImagePullBackOff for ten minutes hasn't hung. It's waiting for the next attempt, and its STATUS toggles between the two values.

InvalidImageName and ErrImageNeverPull never touch the network at all: kubelet refuses before it even reaches out to the registry.

What the diagnostics show

kubectl describe pod web

Two parts of the output matter.

Containers → Image: what image was actually requested.

Containers:
  app:
    Image:          nginxinc/nginx-unpriviledged:1.27
    State:          Waiting
      Reason:       ImagePullBackOff

Events: what the runtime answered. This has the direct error text (lines shortened):

Events:
  Type     Reason   Message
  Normal   Pulling  Pulling image "nginxinc/nginx-unpriviledged:1.27"
  Warning  Failed   Failed to pull image "nginxinc/nginx-unpriviledged:1.27": ... pull access denied, repository does not exist or may require authorization ...
  Warning  Failed   Error: ErrImagePull
  Normal   BackOff  Back-off pulling image "nginxinc/nginx-unpriviledged:1.27"
  Warning  Failed   Error: ImagePullBackOff

From here, the error text reads almost like a reference guide:

A private registry needs an imagePullSecret: a Secret with registry login credentials, attached to the Pod or to its ServiceAccount. Without it, the Pod falls into the same ErrImagePull and ImagePullBackOff, only the text in Events is different.

If you only need the tail of events without the full describe, filtering by object helps:

kubectl get events --field-selector involvedObject.name=web --sort-by='.lastTimestamp'

Common typos

A quick way to check the theory: pull the same image by hand with docker pull, or look at its manifest with crane manifest. If the image isn't there either, the name really is wrong, and it's not the cluster's fault.

imagePullPolicy and why it affects debugging

A container has an imagePullPolicy field. If you don't set it, Kubernetes fills in a value when the Pod is created:

The third value, Never, is only ever set by hand.

Always means kubelet goes to the registry every time the container starts. If the registry is unreachable, the new container won't start, even if the image is already on the node.

IfNotPresent pulls only if that image isn't on the node yet. This is more reliable, but the first run on a new node still goes to the registry.

Never means "the image is already on the node, don't go anywhere". If it's not there, the status will be ErrImageNeverPull. This mode is used in local clusters (kind, minikube), where the image is loaded by a separate command, for example kind load docker-image.

How to fix it

The field spec.containers[*].image on a Pod can be changed on a live Pod, so the name gets fixed in place:

kubectl set image pod/web app=nginxinc/nginx-unprivileged:1.27.3

Here, app is the container's name inside the Pod, not the Pod's name. You can do the same thing through an editor:

kubectl edit pod web

After the change, kubelet immediately tries to pull the new image: the pause from past failures doesn't carry over to the new name. Within a few seconds, the Pod becomes Running.

Fixing a Pod created by a Deployment this way is pointless. The Deployment creates Pods from its own spec.template, and any new Pod (restart, rollout, scaling) will come back with the old image. So you fix the template instead:

kubectl set image deployment/web app=nginxinc/nginx-unprivileged:1.27.3

The Deployment creates a new ReplicaSet, and the Pods are recreated with the fixed image. You make the permanent fix where the manifest came from: in the YAML in the repository, in the Helm chart's values. Otherwise the next deploy brings the typo back.

How to prevent it

It's convenient to check the image name before applying the manifest: docker pull or crane manifest respond in seconds. In CI, the same effect comes from a step that checks the image exists in the registry before the deploy.

A floating tag together with imagePullPolicy: Always makes every container restart depend on the registry. A specific tag and IfNotPresent remove that dependency.

Homework

Practice the theory on your own cluster: 1 step with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.

./run.sh start debug-image     # set up the starting state
./run.sh verify debug-image    # check the current step

In short

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-debug-01-imagepullbackoff