Docker push manifest invalid from a registry
A docker push manifest invalid failure is the registry refusing the document that describes your image, not the layers, and the reason sits in a detail field that no Docker client prints. Fetch the response body yourself, or change the media types buildx writes, because those are the only two moves that touch the actual disagreement.

What this error means
Layers upload normally, sometimes all of them, and the step dies on the last request of the push. The wording depends on which client pushed. BuildKit and buildx surface the transport failure and the status code. The classic Docker daemon surfaces the registry error code instead, which renders as the word "manifest invalid" printed twice, because the code and its default message are the same phrase. Neither one prints the sentence that says which media type was refused. We have not recorded a run for this page, and the block below is assembled from the strings each component formats rather than captured from a job.
#14 exporting manifest list sha256:c3b9d1a2
#14 ERROR: failed to push acme/api:1.4.2: unexpected status from PUT request to https://registry.example.internal/v2/acme/api/manifests/1.4.2: 400 Bad Request
--- the body that came with that 400, fetched separately with curl
{"errors":[{"code":"MANIFEST_INVALID","message":"manifest invalid","detail":"unsupported manifest media type and no default available: application/vnd.oci.image.index.v1+json"}]}The useful sentence is in the body, and the client throws it away
A registry built on the distribution project answers a manifest upload it cannot parse with HTTP 400 and the error code MANIFEST_INVALID. The code carries a default message that happens to read "manifest invalid", and the specific reason goes in a separate detail field.
Then the client drops it. In the distribution client library an error renders as its code followed by its message, and the Detail property is not part of that rendering at all, so a structured error whose detail names the exact media type prints as "manifest invalid: manifest invalid". The containerd path does not do better: its unexpected-status error stores the response body on the struct and formats only the method, the URL and the status. So the answer is on the wire and not in your log.
| Where the string is formed | What reaches the Actions log |
|---|---|
Registry, PutManifest in the distribution handlers | HTTP 400, code MANIFEST_INVALID, message "manifest invalid", detail naming the type |
Registry, UnmarshalManifest in distribution | "unsupported manifest media type and no default available: " and the type, as the detail |
| Distribution client, its error rendering | The code and the message only, so "manifest invalid: manifest invalid" |
| containerd remotes, its unexpected-status error | Method, URL and status only, so "400 Bad Request" with no reason |
Common causes
The registry does not map the OCI media type you pushed
The registry looks the request Content-Type up in a table of unmarshal functions, and a registry old enough to predate OCI image indexes has no entry for one. There is no fallback unless a default is registered, which is exactly what the detail sentence says when it ends "and no default available". In our experience this is the common shape behind a 400 on the last request of a push.
Attestations pulled the whole export into OCI types
Provenance and SBOM attestations cannot be described in the Docker manifest format, so an export that carries them uses OCI media types regardless of what you would otherwise have got. A workflow that turned on provenance for supply-chain reasons can start failing against a registry that was fine the week before, with no change to the Dockerfile.
A BuildKit upgrade changed the default
BuildKit pins media-type behavior to a compatibility version. On the current path as of 2026-09-20, an image export that does not set oci-mediatypes defaults to OCI media types. On the historical path that covers v0.15.0 through v0.31.x, an export without attestations defaulted to Docker media types instead. Upgrading the builder therefore changes what you push without changing your workflow.
The manifest is schema 1 and something in the chain refuses it
This is the other direction and it is usually a pull rather than a push. A base image published years ago can still be served as application/vnd.docker.distribution.manifest.v1+prettyjws, which modern clients decline. The wording overlaps with the push case enough to be confusing, and the fix has nothing to do with your exporter.
How to fix it
Write Docker media types from the image exporter
The exporter takes oci-mediatypes as a boolean option. Setting it false makes buildx write the classic manifest and manifest list formats, which is what a registry that refused the OCI index is asking for. Set it on the output rather than globally, so a second push to a modern registry is unaffected.
docker buildx build \
--output type=image,name=registry.example.internal/acme/api:1.4.2,oci-mediatypes=false,push=true \
.Turn off the attestations that force OCI
- Add
--provenance=falseand--sbom=falseto the build that pushes to the strict registry. - Keep them on for the registry that accepts them, if you publish to both.
- Treat this as a concession, not a cleanup: you are giving up supply-chain metadata to satisfy a registry.
- uses: docker/build-push-action@v7
with:
push: true
provenance: false
sbom: false
tags: registry.example.internal/acme/api:1.4.2
outputs: type=image,oci-mediatypes=falsePin the exporter behavior across a BuildKit upgrade
The compatibility-version exporter option pins the digest-affecting image assembly behavior, which includes the media-type default. Pinning it makes the upgrade that changes the default a deliberate step rather than something you find out about from a registry.
--output type=image,name=acme/api:1.4.2,push=true,compatibility-version=20For the schema 1 direction, replace the image
If the type in the detail ends manifest.v1+prettyjws, nothing about your exporter is involved. The image on the other end is in a format current clients removed support for. Repoint the reference at a maintained tag, or mirror the old image through a registry that converts it, and leave your push settings alone.
docker buildx imagetools inspect --raw quay.io/coreos/etcd:v3.3.10 | head -5Get the detail out of the registry yourself
Because no client prints it, the fastest diagnosis is to make the same request by hand and read the body. A HEAD against the tag is enough to see the negotiation, and a PUT replay is rarely necessary once you know which types the registry advertises it will accept.
Do this from the job that failed rather than from your laptop, because the token, the registry host and the proxy are all part of what you are testing.
- name: Ask the registry what it says
run: |
TOKEN=$(echo "${{ secrets.REGISTRY_TOKEN }}")
curl -sS -i -X HEAD \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.oci.image.index.v1+json" \
"https://registry.example.internal/v2/acme/api/manifests/1.4.2"What "unsupported media type" does and does not point at
Two strings in this area are about the old schema 1 format, not about OCI, and reading them backwards sends you to the wrong fix. The distribution sentence "unsupported manifest media type and no default available" appears with application/vnd.docker.distribution.manifest.v1+prettyjws in m3db/m3#4396, where a modern daemon refuses to pull a 2018-era image. The go-containerregistry library has its own "unsupported MediaType" error, and in that library it is raised only for schema 1 manifests, with a link to the issue explaining that the project will not support them.
So when you find either phrase while searching, check the direction before you act. Pulling an ancient image is a schema 1 problem and the fix is a newer image. Pushing a fresh buildx image to a registry that will not take it is an OCI problem, and the fix is on the exporter.
The builder version decides the default, so pin it
BuildKit pins the behavior that affects an image digest, media types included, to a compatibility version you can set as an exporter option. Three values exist at the time of writing and only the newest one defaults an unset oci-mediatypes to OCI, which is why a builder upgrade is a plausible answer to "nothing changed and it broke".
Set it on the push that goes to the strict registry. You are not freezing the builder, only the part of its output that the registry has an opinion about.
| BuildKit compatibility version | Default when oci-mediatypes is unset |
|---|---|
30, the current path as of 2026-09-20 | OCI media types |
20, the v0.15.0 through v0.31.x path | Docker media types, unless the export carries attestations |
10, the v0.13.0 and v0.14.0 path | Differs from 20 on git source file modes and on zstd, not on media types |
Why no recorded run backs this page
This failure needs a registry that refuses a format, and every registry the reproduction harness can reach accepts both OCI and Docker manifests. Recording a run would mean standing up a deliberately old registry build, pushing to it, and then presenting the result as though it were a registry you might actually use. That would record our configuration and teach nothing about yours.
The parts that are checkable without a run are checked: the code that returns the status, the function that writes the detail, and the two error types that decline to print it. What a run would add is a screenshot of a log we can already tell you is missing the important line.
How to prevent it
- Pin
compatibility-versionon any push that goes to a registry you do not control. - Push a throwaway tag from a new builder version before you move the real pipeline to it.
- Keep one curl step in the workflow that prints the registry response body on failure.
- Record which of your registries accept OCI indexes, because the log will not tell you later.
Frequently asked questions
Why does the log say manifest invalid twice?
MANIFEST_INVALID becomes "manifest invalid" and its default message is also "manifest invalid", so you get both halves. The part that differs between cases is the detail, which that rendering leaves out.Does oci-mediatypes=false lose anything?
How do I see the detail field the client hides?
errors array, and each entry carries code, message and detail. The detail is the sentence naming the media type. Registry access logs show the status but usually not the body, so the client-side repeat is the quicker route.Is a 400 on a push ever worth retrying?
Related guides
References
- distribution: the manifest handlers that return MANIFEST_INVALID
- distribution: UnmarshalManifest, where the media type detail is written
- BuildKit: compatibility-version and the media type defaults
- Docker docs: the image exporter and its options
- m3db/m3#4396: a schema 1 image refused with the same media type sentence
- Docker documentation
- Docker build cache
- GitHub Actions documentation