Your Pod is not starting, and kubectl get pods shows ErrImagePull or ImagePullBackOff. These statuses usually mean Kubernetes cannot pull the container image from the registry.
The Pod events usually show why the pull failed. This guide explains how to read those events and fix the most common causes. If you are new to Pods, start with What is a Kubernetes Pod?.
ImagePullBackOff is a Pod status. It means the kubelet could not pull the container image and is waiting before trying again. ErrImagePull means the image pull failed. After a failed pull, the Pod can move from ErrImagePull to ImagePullBackOff while the kubelet waits and retries.
The container stays in the Waiting state because the image has not been pulled successfully, so the container cannot start. Both statuses usually have the same causes and fixes.
If the RESTARTS count is increasing, the container has started and is being restarted. That points to a different problem, such as CrashLoopBackOff, rather than an image-pull failure.
kubectl get podsNAME READY STATUS RESTARTS AGE
badtag 0/1 ErrImagePull 0 36s
An image-pull problem usually shows 0 restarts because the container has not started yet.
The Events section at the bottom of kubectl describe usually shows the actual reason for the failed image pull. For an interactive terminal view, read our K9s guide.
kubectl describe pod badtag | grep -A 20 '^Events:'
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 37s default-scheduler Successfully assigned default/badtag to kubenative-control-plane
Normal Pulling 23s (x2 over 36s) kubelet spec.containers{badtag}: Pulling image "nginx:1.29.99"
Warning Failed 20s (x2 over 35s) kubelet spec.containers{badtag}: Failed to pull image "nginx:1.29.99": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/library/nginx:1.29.99": failed to resolve reference "docker.io/library/nginx:1.29.99": docker.io/library/nginx:1.29.99: not found
Warning Failed 20s (x2 over 35s) kubelet spec.containers{badtag}: Error: ErrImagePull
Normal BackOff 5s (x2 over 34s) kubelet spec.containers{badtag}: Back-off pulling image "nginx:1.29.99"
Warning Failed 5s (x2 over 34s) kubelet spec.containers{badtag}: Error: ImagePullBackOff
The line that starts with Failed to pull image is the important one. Look at the end of that message to see what the registry reported. Here, the registry reports:
not found
That tells you the image or tag does not exist.
Events can be harder to read after the Pod has been failing for a long time. You may only see Back-off pulling image in the latest events, while the Pod still keeps the last image-pull error in its status. You can check that error with:
kubectl get pod badtag \
-o jsonpath='{.status.containerStatuses[0].state.waiting.message}' | grep -o 'failed to resolve.*'
failed to resolve reference "docker.io/library/nginx:1.29.99": docker.io/library/nginx:1.29.99: not foundThe exact wording can vary between container runtimes and registries, but the underlying cause is usually the same.
After each failed pull, the kubelet waits longer before trying again. The planned wait starts at 10 seconds and doubles after each failure, until it reaches 300 seconds. The planned waits are 10, 20, 40, 80, 160, and then 300 seconds for every later attempt.
Here are the measured gaps between pull attempts for a Pod with a tag that does not exist:
Each measured gap is a little longer than the planned wait. The kubelet checks the Pod again only every 10 to 15 seconds, and the pull attempt also takes time. So treat the planned wait as the minimum wait.
The kubelet tracks the wait for each Pod and image pair, so a new Pod starts with a fresh wait. A Pod that is waiting for a missing Secret does not start immediately when you create the Secret. It starts at the next attempt. For example, a Pod that had already waited 7 minutes needed another 233 seconds. If you delete the waiting Pod, the replacement starts with a fresh wait.
The image name has a typo, the tag was never pushed, or someone deleted the tag. The event ends with not found, which means the registry does not have that image.
You have to fix it by pointing the Deployment to a tag that exists:
kubectl set image deployment/shop nginx=nginx:1.29
deployment.apps/shop image updated
kubectl get pods -l app=shop
NAME READY STATUS RESTARTS AGE
shop-c87878c54-f4rp9 1/1 Running 0 6s
shop-f489f7b7d-vgc6x 0/1 Terminating 0 26s
The Deployment started a new Pod, and the new Pod pulled the image successfully. In a manifest, the same fix is one line:
containers:
- name: nginx
image: nginx:1.29
If the tag exists but the pull still fails, go back to the table in Step 3.
The image is in a private registry, but the Pod has no credentials to access it. The event says pull access denied, repository does not exist or may require authorization. The message lists both possibilities, so the event alone cannot tell you which one is true. Check both the image name and the Secret.
The credentials are stored in a Secret of type kubernetes.io/dockerconfigjson. Create one with kubectl create secret docker-registry:
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com:5000 \
--docker-username=<username> \
--docker-password=<password>
secret/regcred createdThen add the Secret to the Pod spec:
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: registry.example.com:5000/team/api:v1
imagePullSecrets:
- name: regcredA running Pod cannot add imagePullSecrets, because the API server allows changes to only a few Pod fields. A Deployment solves this because changing the Pod template starts new Pods.
You can also attach the Secret to a ServiceAccount, so every new Pod that uses that ServiceAccount gets the Secret:
kubectl patch serviceaccount default -n prod -p '{"imagePullSecrets": [{"name": "regcred"}]}'
serviceaccount/default patchedThe Pod uses a Secret, but the image pull still fails. You should check the following three things:
a. The Secret is in the wrong namespace: A Pod can only use Secrets from its own namespace. The Pod below runs in team-a, but the Secret exists only in default. The kubelet shows this in an event:
kubectl describe pod web -n team-a | grep -o 'Unable to retrieve.*'
Unable to retrieve some image pull secrets (regcred); attempting to pull the image may not succeed.Create the Secret in team-a using the same kubectl create secret docker-registry command and add -n team-a. The Pod started a few seconds later:
kubectl get pods -n team-a
NAME READY STATUS RESTARTS AGE
web 1/1 Running 0 39sb. The password is wrong or expired: The message ends with 401 Unauthorized:
kubectl describe pod web -n team-b | grep -o 'unexpected status.*'
unexpected status from HEAD request to http://registry.example.com:5000/v2/team/api/manifests/v1: 401 Unauthorized
You have to delete the Secret and create it again with the correct values. Tokens from cloud registries can expire. An Amazon ECR token is valid for 12 hours, so a Secret created by hand stops working after 12 hours. You can let the kubelet fetch fresh credentials with a credential provider plugin.
c. The registry name does not match: The Secret stores credentials for a specific registry name. That name must match the registry name in the image. Decode the Secret to compare them:
kubectl get secret regcred -n hostmismatch --output="jsonpath={.data.\.dockerconfigjson}" | base64 --decode | jq ".auths | keys"
[
"registry.example.org:5000"
]The image comes from registry.example.com:5000, so the kubelet finds no credentials for that registry. The Pod then shows the same message as Cause 2.
Since Kubernetes 1.35, the kubelet checks credentials even for an image that is already on the node. The feature gate is KubeletEnsureSecretPulledImages.
Docker Hub limits image pulls. Nodes that pull without logging in and share the same public IP address also share the same allowance. Logged-in pulls count for each account. The event contains You have reached your pull rate limit, which Docker Hub returns with HTTP status 429.
Docker publishes these limits, and they can change:
A pull of a multi-architecture image counts once for each architecture.
You can fix it by logging in to Docker Hub. Create a Secret for Docker Hub and attach it to the ServiceAccount, as shown in Cause 2:
kubectl create secret docker-registry dockerhub \
--docker-server=https://index.docker.io/v1/ \
--docker-username=<username> \
--docker-password=<access-token>
secret/dockerhub created
Next, run the following command to patch the ServiceAccount:
kubectl patch serviceaccount default -n <namespace> -p '{"imagePullSecrets": [{"name": "dockerhub"}]}'serviceaccount/default patchedYou have to look for one of these four messages in the Pod events:
Test from the node, not from your laptop, because they may use different networks. The command kubectl debug node starts a Pod on the node, and crictl pulls the image through the container runtime:
kubectl debug node/kubenative-control-plane --image=busybox:1.37 --profile=sysadmin -- chroot /host crictl pull registry.invalid/team/api:v1
Creating debugging pod node-debugger-kubenative-control-plane-hbkl4 with container debugger on node kubenative-control-plane.
kubectl logs node-debugger-kubenative-control-plane-hbkl4 | grep fatal | grep -o 'lookup.*host'
lookup registry.invalid on 192.168.65.254:53: no such host
This check requires crictl to be installed on the node. When you finish, delete the debug Pod:
kubectl delete pod node-debugger-kubenative-control-plane-hbkl4 --now
You have to fix it based on the cause: for DNS, correct the registry name or the node's DNS settings. For a refused connection, correct the address or port, or start the registry. For a timeout, fix the firewall or route. For a certificate error, give the node the registry CA.
For containerd, use a hosts.toml file. In the containerd config.toml file, set config_path to /etc/containerd/certs.d. Then put hosts.toml in a folder named after the registry, as in the path below. The setting is under [plugins."io.containerd.cri.v1.images".registry] in containerd 2.x and under [plugins."io.containerd.grpc.v1.cri".registry] in containerd 1.x. Setting config_path for the first time needs a containerd restart.
The image exists and the credentials work, but the registry has no build for the CPU architecture of the node. The event says no match for platform in manifest. This happens when you build an image for amd64 but run it on arm64, or the reverse.
Check the architecture of the node:
kubectl get nodes -o custom-columns=NAME:.metadata.name,ARCH:.status.nodeInfo.architecture
NAME ARCH
kubenative-control-plane arm64
You can fix it by building the image for both CPU architectures, for example with Docker Buildx:
docker buildx build --platform linux/amd64,linux/arm64 -t <image> --push .
With the new multi-architecture image, the Pod ran on the arm64 node:
kubectl get pod legacy-multiarch -n arch
NAME READY STATUS RESTARTS AGE
legacy-multiarch 1/1 Running 0 8m56sThese two statuses can look like image-pull errors, but they involve no registry contact.
a. InvalidImageName means the kubelet cannot parse the image name. Repository names must be lowercase:
kubectl describe pod upper -n names | grep -o 'Failed to apply.*'
Failed to apply default image tag "Nginx:1.29": couldn't parse image name "Nginx:1.29": invalid reference format: repository name (library/Nginx) must be lowercase
b. ErrImageNeverPull means the Pod has imagePullPolicy: Never, but the image is not on the node:
kubectl describe pod never -n names | grep -o 'Container image.*Never'
Container image "nginx:1.29" is not present with pull policy of Never
You have to fix the image name, set imagePullPolicy to IfNotPresent, or load the image onto the node. A running Pod does not allow you to change imagePullPolicy, so create the Pod again.
A Pod with Never can start without pulling the image once another Pod has already loaded that image onto the same node.
A Deployment pulls an image from a private registry. Start with no Secret:
kubectl create deployment api --image=registry.example.com:5000/team/api:v1
deployment.apps/api created
kubectl get pods -l app=api
NAME READY STATUS RESTARTS AGE
api-5fb9f68cf9-8pzgg 0/1 ErrImagePull 0 30s
The image was not pulled. Read the event:
kubectl describe pod -l app=api | grep -o 'pull access denied.*'
pull access denied, repository does not exist or may require authorization: authorization failed: no basic auth credentials
The words pull access denied point to Cause 2. Check whether the Pod has an image pull Secret:
kubectl get pod -l app=api -o jsonpath='{.items[0].spec.imagePullSecrets}'
The command prints nothing, so the Pod has no Secret. Create the Secret and add it to the Pod template:
kubectl create secret docker-registry regcred --docker-server=registry.example.com:5000 --docker-username=<username> --docker-password=<password>
secret/regcred created
kubectl patch deployment api -p '{"spec":{"template":{"spec":{"imagePullSecrets":[{"name":"regcred"}]}}}}'
deployment.apps/api patched
kubectl wait --for=condition=Available deployment/api --timeout=60s
deployment.apps/api condition met
kubectl get pods -l app=api
NAME READY STATUS RESTARTS AGE
api-5fb9f68cf9-8pzgg 0/1 Terminating 0 31s
api-d9d9c6765-m4dsh 1/1 Running 0 1s
The patch changed the Pod template, so the Deployment created a new Pod. The new Pod had a fresh pull backoff and could pull the image immediately.
These practices can help to prevent image pull problems before they happen:
a. Use version tags: Kubernetes advises against using the latest tag in production because it is hard to know which version is running. For exact repeatability, pin the image digest.
b. Know the default pull policy: Kubernetes sets imagePullPolicy when it creates the Pod and does not change it later, even if you change the image. The defaults are:
c. Put the Secret on the ServiceAccount: This allows new workloads to get the Secret automatically.
d. Check that the image exists: You should verify the image in your pipeline before the deploy step runs.
ImagePullBackOff means the kubelet could not pull the image and is waiting before it tries again. Run kubectl describe pod, read the end of the "Failed to pull image" line, match it to one of the six causes, and apply that fix.
After the fix, start a new Pod instead of waiting, because the wait can grow to 300 seconds. To stop the error coming back, use version tags and put the Secret on the ServiceAccount.
ErrImagePull means the image pull failed. ImagePullBackOff means the kubelet is waiting before trying again. Both can have the same causes and fixes.
The kubelet keeps trying until the image pull works. The wait grows up to 300 seconds between attempts.
The kubelet retries only when the current wait ends, and the wait can reach 5 minutes. Start a new Pod to get a fresh wait: delete the Pod or change the Deployment template.
No. The default image pull backoff starts at 10 seconds and grows to 300 seconds. These waits are not set through the normal kubelet configuration.
Delete the Pod, and the Deployment creates a new Pod with a fresh wait. For a bare Pod, change the image field instead. The new image reference starts with a fresh wait, so the kubelet tries immediately.
Yes. A Pod can only use Secrets from its own namespace.
The nodes may have different CPU architectures, different network access to the registry, or different registry trust settings. Check the node and registry configuration when the same image works on one node but not another.
No. If the image is not already on the node, the Pod shows ErrImageNeverPull.