Quick answer
One message, four causes — check in this order:
- Unqualified name —
myappmeansdocker.io/library/myapp. Yours isdocker.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. Rundocker 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:
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 type | What Docker actually requests |
|---|---|
myapp | docker.io/library/myapp:latest — official images only |
youruser/myapp | docker.io/youruser/myapp:latest — your Docker Hub repo |
ghcr.io/yourorg/myapp | ghcr.io/yourorg/myapp:latest — no rewriting; needs a GHCR login |
myapp:1.4.0 | docker.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:
| Registry | Login command |
|---|---|
| Docker Hub | docker login |
| GitHub Container Registry | docker login ghcr.io -u YOUR_USERNAME — password is a PAT with read:packages |
| Quay | docker login quay.io |
| Amazon ECR | aws ecr get-login-password --region REGION | docker login --username AWS --password-stdin ACCOUNT.dkr.ecr.REGION.amazonaws.com |
| Google Artifact Registry | gcloud 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
| Message | What it means | Fix |
|---|---|---|
| pull access denied … may require 'docker login' (this page) | Repo is missing or unreadable — deliberately indistinguishable | Work the decision tree above |
manifest for X:tag not found | Repo is readable; that tag doesn't exist | Fix the tag — credentials are fine |
denied: requested access to the resource is denied (on push) | Authenticated, but no write permission | Check push rights / repo ownership |
unauthorized: incorrect username or password | The docker login itself failed | Use a token, not a password (required with 2FA) |
toomanyrequests: You have reached your pull rate limit | Docker Hub rate limit — not an auth failure | Authenticate 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
- ✓ Read the resolved reference in the error — a bare name became
docker.io/library/<name> - ✓ Fully qualify the image:
registry/namespace/image:tag - ✓ Re-read the name and tag character by character — typos look identical to permission failures here
- ✓ Test public reachability:
docker logout <registry>then pull again - ✓ Log in to the specific host — a Docker Hub login doesn't cover
ghcr.ioorquay.io - ✓ Use a scoped access token via
--password-stdin, never a bare password - ✓ Still denied after
Login Succeeded? Check token scopes, org membership, and (for ECR/GCR) the cloud IAM policy - ✓ CI-only failure? Add an explicit login step; suspect a rotated token or a base image that went private
- ✓ Check which hosts you're authenticated to — but note that with a credential helper configured (
credsStore, the Docker Desktop default)~/.docker/config.jsonholds helper references rather than anauthslist, so an empty-looking file does not mean you're logged out. Confirm via the helper ordocker system info, and don't paste that file into a public issue or log
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
- docker login (Docker Docs)
- Docker Hub repositories (Docker Docs)
More Docker errors & tools
Browse the Docker error cluster, or analyse a Dockerfile for size, caching, and security issues.