DockerDocker

pull access denied, repository does not exist or may require authorization

Docker cannot tell a private repository from a misspelled one, so both give this message. How to work out which you have, and fix the registry login.

easy fix6 min read

the docker error
Error response from daemon: pull access denied for myapp, repository does not exist or may require 'docker login': denied: requested access to the resource is denied

unauthorized: authentication required

denied: permission_denied: write_package

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Check whether the image is public before assuming it is auth

    docker manifest inspect nginx:1.27 > /dev/null && echo reachable

    Swap in your own image reference. If an anonymous manifest inspect succeeds, the repository exists and is public, so the problem is the tag or a typo rather than credentials.

  2. 2

    Log in to the registry the image actually names

    echo "$GITHUB_TOKEN" | docker login ghcr.io -u USERNAME --password-stdin

    A bare image name means Docker Hub. Anything with a host prefix such as ghcr.io or an ECR URL needs a login to that host specifically; being logged in to Docker Hub does nothing for it.

  3. 3

    Confirm which registries you hold credentials for

    cat ~/.docker/config.json

    The auths object lists every registry with a stored credential. If the host in your image reference is not a key there, you are pulling anonymously.

All 8 sections

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:

  1. The image name or tag is wrong.
  2. The repository is private and you are not authenticated to that registry.
  3. 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:

RegistryTo pullTo push
GHCRread:packageswrite:packages
Docker HubPublic: none. Private: ReadRead & Write
ECRecr:BatchGetImage, ecr:GetDownloadUrlForLayerplus 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-server must match the registry host exactly. For Docker Hub it is https://index.docker.io/v1/, not docker.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

  1. docker manifest inspect <image>. Succeeds anonymously → the name or tag is wrong, not auth.
  2. Check the name has a namespace. A bare name means library/, which is official images only.
  3. cat ~/.docker/config.json and confirm the host is in auths.
  4. docker login <host> for that specific registry, with the token on stdin.
  5. Push failing but pull working → the token lacks write scope.
  6. ECR failing at login → the IAM policy is missing ecr:GetAuthorizationToken.
  7. GitHub Actions → add permissions: packages: write.
  8. Kubernetes → create an imagePullSecret in 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.

Reference and practice

Learn the underlying concept

Other Docker errors