Docker failed to fetch oauth token during a build
Docker failed to fetch oauth token means the builder asked a registry for permission to pull your base image and was refused, before a single layer moved. The word in front of token is the diagnosis, and our recorded run shows it: the same Dockerfile printed anonymous token with no credential in the client config and oauth token once the script had written a deliberately fake credential into that config for the registry to reject.


What this error means
A build fails within a second or two at the FROM line, or at a COPY --from that names an image, and no build step runs. The message wraps whatever the token endpoint answered, so the useful parts are scattered: the word before "token", the registry host in the URL, and the status code at the end. Our recorded run put three builds on one runner to separate them, and turned up a case nobody documents: the host in the error is not always the host in the Dockerfile.
--- no credential in the client config
ERROR: failed to build: failed to solve: failed to fetch anonymous token: unexpected status from GET request to https://ghcr.io/token?scope=repository%3Alatchkeyci%2Fprivate-base-does-not-exist-9f3c1a%3Apull&service=ghcr.io: 403 Forbidden
--- same build, now with a credential the registry rejects (ghcr.io)
ERROR: failed to build: failed to solve: failed to fetch oauth token: denied: deniedReproduced on a Latchkey runner
Docker version 29.7.2, build a7dcaa6
github.com/docker/buildx v0.36.1 1d8dde89b8aba914e05e45366770736fea1fd690
--- no credential in the client config
ERROR: failed to build: failed to solve: failed to fetch anonymous token: unexpected status from GET request to https://ghcr.io/token?scope=repository%3Alatchkeyci%2Fprivate-base-does-not-exist-9f3c1a%3Apull&service=ghcr.io: 403 Forbidden
--- writing a fake credential into ~/.docker/config.json
--- same build, now with a credential the registry rejects (ghcr.io)
ERROR: failed to build: failed to solve: failed to fetch oauth token: denied: denied
--- same build against Docker Hub, which reports the status code
ERROR: failed to build: failed to solve: latchkeyci/private-base-does-not-exist-9f3c1a:1.0: failed to resolve source metadata for docker.io/latchkeyci/private-base-does-not-exist-9f3c1a:1.0: unexpected status from HEAD request to https://179351861270.dkr.ecr.us-east-1.amazonaws.com/dockerhub/v2/latchkeyci/private-base-does-not-exist-9f3c1a/manifests/1.0?ns=docker.io: 401 Unauthorized
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)The runner diagnosed the failure and did not retry it; this failure needs the fix below.
Anonymous token and oauth token are different diagnoses
A registry pull is two requests: the builder asks a token endpoint for a token scoped to the repository it wants, then uses that token to ask for the manifest. This error is the first request failing, so nothing in your Dockerfile beyond the image name matters yet.
The distinction our run recorded is the part worth taking away. With no credential for that registry in the client configuration, the builder asks anonymously and reports an anonymous token failure. With a credential present and refused, it reports an oauth token failure. So the message answers a question you would otherwise spend an hour on: did the login step reach this build at all.
| What the line says | What reached the builder | Where to look |
|---|---|---|
failed to fetch anonymous token | No credential for that registry | The login step, its order, and its registry host |
failed to fetch oauth token | A credential the registry refused | The token scope and the package permissions |
| A host you never wrote | A registry mirror answered | docker info and the daemon configuration |
| A failure late in a long build | A token that expired mid-build | Token lifetime against build duration |
Common causes
No credential for that registry reached the build
The anonymous wording, and the most common cause in CI. The login step is missing, it ran in a different job, it authenticated a different host than the image reference names, or it ran after the build. Our recorded run reproduces it exactly: an empty client configuration, a private-shaped reference, and a refusal from the token endpoint with no credential offered.
The credential is there and the registry refuses it
The oauth wording. A token with no read scope for that package, a workflow token in a repository the package has not granted access to, or a secret that expired all produce a login that looks fine followed by this. The registry says only that you are denied, which is deliberate: naming the reason would let anyone map private repositories.
The token expired during the build
Registry tokens are short lived, and a build that takes longer than the lifetime can start authorized and finish unauthorized, usually at the push or an export step rather than the FROM line. docker/buildx#3977 reports this shape against a cloud registry with a credential helper, where the same image pushes fine by hand immediately afterwards.
A mirror or proxy answered instead of the registry
Where a daemon has a registry mirror, the reference in your Dockerfile is resolved through another host, and that host has its own authentication. Our recorded run hit this on the Docker Hub build: the log quotes a pull-through cache host, not Docker Hub, so a Docker Hub credential would not have helped.
How to fix it
Put the login first, for the registry the image names
- Add the login step to the same job as the build, before the build step.
- Match the registry in the login to the host in the
FROMline, not to the push target. - Give the job the read permission for packages when using the built-in token.
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v7Check the scope, not the login result
A successful login and a permitted pull are different facts. With the built-in token, the package has to grant this repository access; across organizations, use a token carrying the read:packages scope and store it as a secret rather than widening the workflow token.
permissions:
contents: read
packages: readProve the reference by hand before changing the workflow
One pull from the runner, printed next to the reference the build used, separates a name problem from an access problem in a few seconds. If an unauthenticated pull works from a machine with no credentials, the image is public and your build is looking somewhere else.
echo "base image: [$BASE]"
docker logout ghcr.io
docker pull "$BASE" # anonymous, on purposeHandle the expiring token as a duration problem
When the failure lands late in a long build rather than at the FROM line, the credential is not wrong, it is stale. Shorten the build, or split the push into a step that authenticates immediately before it runs.
- uses: docker/build-push-action@v7
with: { context: ., push: false, load: true }
- uses: docker/login-action@v4 # fresh, right before the push
with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }
- run: docker push ghcr.io/acme/api:${{ github.sha }}Log in before the builder needs it, in the same job
A login writes a credential into the client configuration on that runner, and the builder reads it from there. That gives the ordering rule: the login has to come before the build in the same job, because a different job runs on a different runner with an empty configuration. It also gives the scope rule: the credential has to be for the registry in the image reference, not for the registry you push to.
For an image in GitHub Container Registry the built-in token is enough when the package grants this repository access. The GitHub documentation states that the workflow token can be used if the repository is granted read access to the package, and that a personal access token needs the read:packages scope to download container images, which is the fallback for a package that belongs to another organization.
permissions:
contents: read
packages: read
steps:
- uses: actions/checkout@v7
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
context: .The host in the error is not always the host in your Dockerfile
The third build in our recorded run aimed at Docker Hub and never got there. The log shows a request to an ECR pull-through cache host instead, answering 401 on a HEAD for the manifest, because the runner we used resolves Docker Hub references through a registry mirror. We expected a Docker Hub token endpoint in that line and got a mirror, which is worth disclosing rather than tidying away.
The lesson generalizes. Where a daemon has a mirror configured, a credential for the upstream registry is not one for the mirror, and the error names the host that refused you. Read that host before hunting for the wrong secret.
docker info --format '{{.RegistryConfig.Mirrors}}'
# the error quotes the host that refused, not the one in your FROM lineWhat the runner does about it
No repair. On the recorded run the wrapper posted the failure to the sidecar, the sidecar answered, and the build ended on the same refusal. Latchkey carries no pattern for this, which is correct: a credential the job was never given is not something a runner should conjure, and retrying an authorization failure only spends the queue wait again.
The one case where a retry is right is the expiring token in docker/buildx#3977: the credential was valid at the start of the build and stale at the push. That is a shorter build or a fresher token, not a repair.
How to prevent it
- Keep the login step and the build step in one job, login first.
- Authenticate the registry in the FROM line, not only the push target.
- Grant packages read at the job level rather than widening a token.
- Print the base image reference and the registry mirrors once per job.
Frequently asked questions
What does failed to fetch oauth token mean in a docker build?
Why does my build say failed to fetch anonymous token?
Does docker login work for buildx builds?
Why does the error name a registry I never used?
docker info before assuming the credential for the upstream registry was the one being refused.Related guides
References
- GitHub docs: working with the Container registry, tokens and scopes
- Docker docs: docker buildx build
- docker/buildx#3977: a token that expires during a long build
- microsoft/vscode-remote-release#6920: the same failure with a 403 from the token endpoint
- Docker documentation
- Docker build cache
- GitHub Actions documentation