pull access denied for X, repository does not exist or may require 'docker login' is deliberately vague. Docker will not tell an anonymous caller whether a private repository exists, because that would let anyone enumerate private image names. So a typo and a permissions problem produce the same message.
Three possibilities, in the order they actually occur:
- The image name or tag is wrong.
- The repository is private and you are not authenticated to that registry.
- You are authenticated, to the wrong registry.
Check whether it is public first
docker manifest inspect nginx:1.27 > /dev/null && echo reachable
Run it against your own reference. If an anonymous inspect succeeds, the repository is public and exists, so the problem is a typo or a tag that was never pushed. If it fails the same way, move on to credentials.
Be precise about the name. These are all different repositories:
myapp → docker.io/library/myapp (official images namespace)
acme/myapp → docker.io/acme/myapp
ghcr.io/acme/myapp → GitHub Container Registry
A bare name without a slash resolves into the library/ namespace, which only holds Docker Official Images. Pulling a personal image by bare name therefore always fails, and the message gives no hint that the namespace was the problem.
Check where you are logged in
cat ~/.docker/config.json
{
"auths": {
"https://index.docker.io/v1/": {},
"ghcr.io": {}
},
"credsStore": "desktop"
}
The keys of auths are the registries you have credentials for. If the host in your image reference is not there, you are pulling anonymously. An empty {} value is normal when a credential helper such as desktop or osxkeychain holds the actual secret.
Fix: Log in to the right registry
Each registry has its own flow. Note that all of them take the token on stdin rather than as an argument, which keeps it out of your shell history and out of the process list.
# Docker Hub, with a Personal Access Token, not your password
echo "$DOCKERHUB_TOKEN" | docker login -u myuser --password-stdin
# GitHub Container Registry, needs read:packages on the token
echo "$GITHUB_TOKEN" | docker login ghcr.io -u myuser --password-stdin
# Amazon ECR, token is valid for 12 hours
aws ecr get-login-password --region eu-west-1 \
| docker login --username AWS --password-stdin \
123456789012.dkr.ecr.eu-west-1.amazonaws.com
# Google Artifact Registry
gcloud auth configure-docker europe-west1-docker.pkg.dev
Docker Hub has required an access token rather than an account password for CLI logins for some time. If your password is being rejected and you are sure it is right, that is why: create a token in account settings and use that.
The permission the token needs
Being logged in is not the same as being allowed. Token scopes trip people up constantly:
| Registry | To pull | To push |
|---|---|---|
| GHCR | read:packages | write:packages |
| Docker Hub | Public: none. Private: Read | Read & Write |
| ECR | ecr:BatchGetImage, ecr:GetDownloadUrlForLayer | plus ecr:PutImage, ecr:UploadLayerPart |
A GHCR push failing with denied: permission_denied: write_package while pulls work is a token with read:packages and not write:packages.
ECR is the one with an extra trap: ecr:GetAuthorizationToken is required to log in at all, and it is a separate action from the pull permissions. An IAM policy with the pull actions but not that one fails at the login step, before it ever reaches the image.
In GitHub Actions
Use the built-in token rather than a PAT, and grant the job the scope it needs:
jobs:
build:
permissions:
contents: read
packages: write
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
packages: write is the line people miss. Since GitHub tightened default token permissions, a workflow without it gets a read-only token and the push fails with denied even though the login succeeded.
A GHCR package also has its own visibility and access list, separate from the repository's. A private package in a public repo still needs an explicitly linked repository or an org-level grant.
On Kubernetes: ErrImagePull with the same message
The kubelet has no access to your laptop's ~/.docker/config.json. It needs the credential as a Secret:
kubectl create secret docker-registry ghcr-creds \
--docker-server=ghcr.io \
--docker-username=myuser \
--docker-password="$GITHUB_TOKEN"
spec:
imagePullSecrets:
- name: ghcr-creds
containers:
- name: app
image: ghcr.io/acme/myapp:1.4.2
Two things that catch people out:
- The Secret is namespaced. It has to exist in the same namespace as the Pod. Deploying the same manifest to a new namespace fails until you copy the Secret across.
--docker-servermust match the registry host exactly. For Docker Hub it ishttps://index.docker.io/v1/, notdocker.io.
To avoid repeating imagePullSecrets in every manifest, attach it to the namespace's default ServiceAccount:
kubectl patch serviceaccount default \
-p '{"imagePullSecrets":[{"name":"ghcr-creds"}]}'
On EKS, GKE and AKS, pulls from the matching cloud registry usually work with no Secret at all, because the node's instance role carries the permission. If those pulls fail, fix the node role rather than adding a Secret.
A checklist
docker manifest inspect <image>. Succeeds anonymously → the name or tag is wrong, not auth.- Check the name has a namespace. A bare name means
library/, which is official images only. cat ~/.docker/config.jsonand confirm the host is inauths.docker login <host>for that specific registry, with the token on stdin.- Push failing but pull working → the token lacks write scope.
- ECR failing at login → the IAM policy is missing
ecr:GetAuthorizationToken. - GitHub Actions → add
permissions: packages: write. - Kubernetes → create an
imagePullSecretin the Pod's own namespace.
Frequently Asked Questions
Why does Docker say a repository does not exist when I know it does?
Because Docker will not confirm the existence of a private repository to an unauthenticated caller. Doing so would let anyone enumerate private image names, so a repository you cannot see and a repository that was never created return the same message. The practical consequence is that a typo and a permissions problem are indistinguishable from the error alone. Run docker manifest inspect against the reference: if it succeeds anonymously the repository is public and the name is the problem, and if it fails the same way you are looking at credentials.
What is the difference between "pull access denied" and "unauthorized: authentication required"?
pull access denied generally means no credential was presented at all, so the registry treated the request as anonymous. unauthorized: authentication required more often means a credential was presented and rejected, typically an expired token. ECR tokens last 12 hours, so a long-running build agent that logged in yesterday hits this reliably. The distinction is not perfectly consistent across registries, but it is a useful first hint about whether to check for a missing login or a stale one.
Why does docker login succeed but the push still fail?
Authentication and authorization are separate. Logging in proves who you are; pushing needs a token scope that permits writing. On GHCR a token with read:packages logs in happily and then fails the push with denied: permission_denied: write_package, because it needs write:packages. In GitHub Actions the equivalent is a job without permissions: packages: write, which receives a read-only token. On ECR, the push actions such as ecr:PutImage and ecr:UploadLayerPart are separate from the pull ones in the IAM policy.
How do I pull a private image in Kubernetes?
Create a docker-registry Secret and reference it from the Pod with imagePullSecrets, because the kubelet cannot see the Docker config on your machine. Two details cause most of the failures: the Secret is namespaced, so it must exist in the same namespace as the Pod and does not follow a manifest into a new one, and --docker-server must match the registry host exactly, which for Docker Hub means https://index.docker.io/v1/ rather than docker.io. To avoid repeating it everywhere, patch the namespace's default ServiceAccount with the pull secret.
Why do my ECR pulls stop working after a few hours?
Because aws ecr get-login-password issues a token that expires after 12 hours. Anything that logged in once and stays alive, such as a self-hosted build agent or a long-lived developer VM, starts failing when that window closes. Re-run the login as a step in each job rather than once during machine setup. Inside EKS this is usually a non-issue, because the kubelet uses the node's IAM role to fetch fresh credentials for ECR automatically and no Secret or login is involved.