Docker: pull access denied for X, repository does not exist or may require 'docker login'

Quick answer

One message, four causes — check in this order:

  • Unqualified namemyapp means docker.io/library/myapp. Yours is docker.io/you/myapp.
  • Typo in the image name or tag — a very common cause, and free to rule out.
  • Wrong registry — a Docker Hub login does nothing for ghcr.io. Run docker login ghcr.io.
  • No permission — logged in, but your account can't see that repository.

The exact error string

$ docker pull myapp
Using default tag: latest
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

// BuildKit / docker buildx phrases it differently, same cause:
// ERROR: failed to solve: myapp: failed to resolve source metadata for
//   docker.io/library/myapp:latest: pull access denied, repository does not
//   exist or may require 'docker login'

// docker compose up wraps it too — the daemon message is the same underneath.

Note what the daemon actually told you: it resolved your bare myapp to docker.io/library/myapp. That expansion is visible in the BuildKit wording and is the single most useful clue in the whole message — more on it in Cause 1.

Why the message is deliberately ambiguous

"Repository does not exist or may require docker login" reads like the daemon couldn't be bothered to check which. In fact the registry refuses to distinguish the two: if a missing repository returned a different response from a private one, anyone could probe a registry for the existence of private image names — a slow but real information leak about a company's unreleased projects.

So the ambiguity is a privacy guarantee, not a diagnostic failure. The practical consequence is that you cannot read the answer off the message; you have to eliminate the causes yourself, cheapest first:

Same message → four causes. Work down. 1. Is the name fully qualified? ghcr.io/org/app — not just app No → Docker resolved it to docker.io/library/app yes 2. Is the image public? Check the registry's web UI Public but still denied? → typo in the name or tag private 3. Logged in to that registry? docker login ghcr.io No → log in to that host Hub login ≠ ghcr.io login yes 4. Logged in, denied anyway — your account lacks repo access The registry answers "denied" the same way for a missing repo and a private one — by design.

These are the four causes worth checking, roughly cheapest first — they aren't mutually exclusive (a typo and a wrong-registry login can both be true), so keep going if fixing one doesn't clear it.

Cause 1: an unqualified name means library/

Docker expands a bare image name into a full reference. myapp becomes docker.io/library/myapp — and library/ is the namespace reserved for Docker's curated official images (nginx, postgres, ubuntu). Almost nobody can push there, so pulling your own image by bare name asks for something that will essentially never exist:

# ❌ resolves to docker.io/library/myapp — the official-images namespace
# docker pull myapp

# ✅ your own image on Docker Hub lives under your username
$ docker pull youruser/myapp:1.4.0

# ✅ anything not on Docker Hub must name the registry explicitly
$ docker pull ghcr.io/yourorg/myapp:1.4.0
$ docker pull quay.io/yourorg/myapp:1.4.0

This bites hardest in a Dockerfile or Compose file copied between projects, where a FROM myapp or image: myapp silently means something entirely different once it leaves the machine that had it cached locally.

Cause 2: a typo in the name or tag

The plain-boring cause, and an easy one to skip past. Because a nonexistent repository and a private one produce identical output, a mistyped name looks exactly like a permissions problem — which sends people to docker login when nothing is wrong with their credentials at all.

Start with the non-destructive checks. Open the repository in the registry's web UI (Docker Hub, the GitHub Packages tab, Quay) and confirm both that it exists and what its visibility is set to — that answers "public or private" without touching your local session. To probe a reference without downloading layers, docker manifest inspect queries the registry directly:

# Resolve the reference against the registry without pulling the image
$ docker manifest inspect ghcr.io/yourorg/myapp:1.4.0

If you still need to prove public access independently, you can temporarily log out and retry — just know this clears your saved credentials for that host, so log back in afterwards:

# Optional diagnostic — clears saved credentials for that registry.
# $ docker logout ghcr.io
# $ docker pull ghcr.io/yourorg/myapp:1.4.0

If the pull succeeds while logged out, the image is public and your problem was never authentication — recheck the exact spelling and tag you used originally. If it fails logged out and you're confident the reference is right, the repository is private: continue to Cause 3.

Watch for the near-misses that survive a casual read: a hyphen versus an underscore, a trailing space pasted from a chat message, :lastest, or an organization name that differs from the GitHub organization by one character.

What each reference form actually resolves to

Every short name is expanded before it leaves your machine. Reading the expansion is usually enough to spot the mistake:

What you typeWhat Docker actually requests
myappdocker.io/library/myapp:latest — official images only
youruser/myappdocker.io/youruser/myapp:latest — your Docker Hub repo
ghcr.io/yourorg/myappghcr.io/yourorg/myapp:latest — no rewriting; needs a GHCR login
myapp:1.4.0docker.io/library/myapp:1.4.0 — still library/; the tag changes nothing

Cause 3: logged in to the wrong registry

Docker stores credentials per registry host. A successful docker login to Docker Hub grants nothing on ghcr.io, quay.io, or a cloud registry — and because the failure message never mentions which host was refused, it's easy to be logged in and still denied. Log in to the specific host:

RegistryLogin command
Docker Hubdocker login
GitHub Container Registrydocker login ghcr.io -u YOUR_USERNAME — password is a PAT with read:packages
Quaydocker login quay.io
Amazon ECRaws ecr get-login-password --region REGION | docker login --username AWS --password-stdin ACCOUNT.dkr.ecr.REGION.amazonaws.com
Google Artifact Registrygcloud auth configure-docker REGION-docker.pkg.dev

Use an access token rather than your account password wherever the registry supports it — tokens are scopeable and revocable, and on Docker Hub a password won't work at all once two-factor authentication is enabled. Pipe it from stdin so it never lands in your shell history:

$ echo "$GITHUB_TOKEN" | docker login ghcr.io -u YOUR_USERNAME --password-stdin
Login Succeeded

Login Succeeded is the confirmation you want — note it proves the credentials are valid, not that they can read the specific repository, which is Cause 4.

Cause 4: authenticated, but without access

If you're definitely logged in to the right host and the reference is definitely correct, the account simply can't see that repository. Check, in roughly this order: that the token's scopes include read access to packages or the registry; that your user is a member of the owning organization or team; that the repository's visibility is what you think it is; and for GHCR specifically, that the package is actually linked to the repository whose permissions you're relying on.

Cloud registries add an IAM layer on top of the Docker login — an ECR pull can fail with this message even after Login Succeeded when the IAM principal lacks ecr:BatchGetImage. The Docker-level credential and the cloud-level authorization are two separate gates.

The CI variant: the same causes, with no saved credentials

This isn't a fifth cause — it's causes 3 and 4 showing up in an environment that never logged in. Your workstation has credentials cached from a login you did months ago; a fresh CI runner starts with nothing. Add an explicit login step before anything that pulls or builds from a private image:

# GitHub Actions — log in to GHCR before build/pull steps
- name: Log in to GHCR
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

The built-in GITHUB_TOKEN has a real limit here. It can read and write packages owned by the same repository, but it will not reach a package in another repository or organization, or one that was never linked to a repository — which is exactly the Cause 4 situation reappearing in CI. For cross-repo or cross-org images, store a PAT with read:packages as a repository or organization secret and use that as the password instead of the automatic token.

A CI job that used to pass and now fails with this message usually means a rotated or expired token, or a base image that flipped from public to private — not a change in your Dockerfile. If the failure is on docker push rather than a pull, you'll typically see the shorter denied: requested access to the resource is denied, which points at write permission specifically.

Lookalike registry errors

MessageWhat it meansFix
pull access denied … may require 'docker login' (this page)Repo is missing or unreadable — deliberately indistinguishableWork the decision tree above
manifest for X:tag not foundRepo is readable; that tag doesn't existFix the tag — credentials are fine
denied: requested access to the resource is denied (on push)Authenticated, but no write permissionCheck push rights / repo ownership
unauthorized: incorrect username or passwordThe docker login itself failedUse a token, not a password (required with 2FA)
toomanyrequests: You have reached your pull rate limitDocker Hub rate limit — not an auth failureAuthenticate to raise the limit, or wait

The rate-limit one is worth recognising on sight: anonymous pulls from Docker Hub are capped, so an unauthenticated CI job that pulls a public base image can start failing purely from volume. Logging in fixes it — but it's a quota problem, not a permissions problem.

Debugging checklist

Frequently Asked Questions

Why does Docker say the repository does not exist OR may require login?

Because the registry deliberately returns the same response for both cases. If it distinguished a missing repository from a private one, anyone could probe for the existence of private image names. That ambiguity is a privacy feature, not a vague error message — which is why you have to rule the causes out yourself rather than reading the answer off the message.

Why does pulling my own image fail when the name looks right?

An unqualified name expands to the official-images namespace on Docker Hub: docker pull myapp becomes docker.io/library/myapp, and library/ holds only Docker's curated official images. Your own image lives under your username or organization, so it must be pulled as docker.io/youruser/myapp — or with the registry included for anything not on Docker Hub, such as ghcr.io/yourorg/myapp.

How do I know whether an image is public or I just lack access?

Try the pull with no credentials in play. Run docker logout for that registry and pull again: if it succeeds logged out, the image is public and your original problem was elsewhere — usually the name or tag. If it fails logged out but the name is definitely right, the repository is private and you need credentials that can see it.

Does docker login cover every registry?

No. Credentials are stored per registry host, so logging in to Docker Hub does nothing for GitHub Container Registry, Quay, ECR, or Artifact Registry. You must run docker login against the specific host — for example docker login ghcr.io — and cloud registries usually issue short-lived tokens through their own CLI instead, such as aws ecr get-login-password piped into docker login.

It works locally but fails in CI — why?

Your machine has credentials saved from an earlier docker login; a fresh CI runner has none. Add an explicit login step before any build or pull that touches a private image, using a token from your CI secret store rather than a password. Expired or rotated tokens produce the same failure on a runner that used to work.

Is this the same as 'toomanyrequests' or 'manifest not found'?

No — those are distinct failures with distinct fixes. A toomanyrequests error means you hit Docker Hub's pull rate limit and should authenticate or wait, not that access was denied. A manifest not found error means the repository was readable but the specific tag does not exist, so the fix is correcting the tag rather than your credentials.

References

More Docker errors & tools

Browse the Docker error cluster, or analyse a Dockerfile for size, caching, and security issues.

All Error References Docker Image Optimizer Docker: cannot connect to the daemon
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.