# Docker containerd image store not enabled in CI

> Docker containerd image store not enabled is why buildx refuses a multi-platform load, and attestations cause the same refusal on one platform.

Source: https://latchkey.dev/learn/docker/docker-containerd-image-store-not-enabled  
Updated: 2026-09-20

Docker containerd image store not enabled is the real condition behind a buildx refusal that never mentions containerd, because buildx decides by reading one field in `docker info` and reports only the consequence. The same refusal fires for a single-platform build that carries attestations, which is why it can appear the day you turn provenance on.

## What this error means

A `docker buildx build --load` fails immediately, before any build step runs, with a sentence about the docker exporter and manifest lists. Nothing in it names containerd, the image store, or a setting you could change. The same command with `--push` instead of `--load` succeeds, which sends people looking at the registry. A second wording exists for builds that write a tar file. The block below sets both out as the two programs that emit them format them, because no run is recorded for this page.

```Both wordings as buildx formats them, not a recorded run
--- buildx refused this before the build started
ERROR: docker exporter does not currently support exporting manifest lists
--- same build with --output type=docker,dest=image.tar
ERROR: docker exporter does not support exporting manifest lists, use the oci exporter instead
```

## Common causes

### The daemon is using the classic image store

The classic store holds one platform per image, so it has no way to keep a manifest list locally. Buildx knows this from the driver status and refuses rather than building something it cannot import. Most Linux CI runners ship this way unless the daemon configuration says otherwise.

### Attestations were turned on and nobody changed the platform list

Provenance and SBOM attestations are described in a format the docker exporter cannot carry, so buildx treats a build with attestations the same way it treats a multi-platform build. In our experience this is the confusing one, because the message talks about manifest lists and the workflow only ever built one platform.

### The build writes a tar file rather than loading

When an output destination is set, buildx cannot substitute the OCI importer, because there is nothing to import into. That path refuses on both image stores and says so in different words, naming the oci exporter as the alternative. Enabling the containerd store does not help here.

### The setting was applied but the daemon was not restarted

The image store is chosen when the daemon starts. Writing the feature flag into the daemon configuration file changes nothing until the daemon reads it again, and a job that edits the file and immediately builds will see the old store. The check in the step above will say so plainly.

## How to fix it

### Turn on the containerd snapshotter for the job

1. Write the feature flag into the daemon configuration file on the runner.
2. Restart the daemon and wait for it to come back before the next step.
3. Assert the driver status shows the containerd snapshotter, so a silent failure to restart fails loudly instead.

```.github/workflows/ci.yml
- name: Enable the containerd image store
  run: |
    echo '{ "features": { "containerd-snapshotter": true } }' \
      | sudo tee /etc/docker/daemon.json
    sudo systemctl restart docker
    timeout 60 bash -c 'until docker info >/dev/null 2>&1; do sleep 2; done'
    docker info --format "{{json .DriverStatus}}" | grep -q snapshotter \
      || { echo "::error::containerd image store did not come up"; exit 1; }
```

### Or stop asking for a local multi-platform image

Most pipelines load an image only to run tests against it, and tests run on one architecture at a time. Load the platform the job will actually use and push the full set separately. This is the cheaper fix and it survives a runner image change.

```Terminal
docker buildx build --platform linux/amd64 --load -t acme/api:test .
docker buildx build --platform linux/amd64,linux/arm64 --push -t acme/api:1.4.2 .
```

### Turn attestations off for the load and keep them for the push

If the build is single-platform and the failure appeared when provenance did, split the concerns. The load exists to run tests and does not need attestations; the push does. Turning them off on the load is not giving anything up, because nothing consumes an attestation from the local image store.

```Terminal
docker buildx build --load --provenance=false --sbom=false -t acme/api:test .
```

### For a tar output, ask for the OCI layout the message names

The second wording tells you the fix outright. The oci exporter writes a layout that holds a manifest list, and tools that consume image archives increasingly read it. Change the output type rather than enabling the image store, because the store is not what blocked this one.

```Terminal
docker buildx build --platform linux/amd64,linux/arm64 \
  --output type=oci,dest=image.tar -t acme/api:1.4.2 .
```

## How to prevent it

- Assert the driver status in the job rather than assuming the runner image kept it.
- Keep the platform list on the load step down to the one the tests run on.
- Decide attestations per output, not once for the whole workflow.
- Wait for the daemon to answer after a restart, so a slow start does not read as the old store.

## The check buildx makes, and what it does with the answer

Before a build starts, buildx asks the daemon for its info and looks through the driver status entries for a pair whose first element is `driver-type` and whose second is `io.containerd.snapshotter.v1`. That single test is what it calls the OCI importer feature.

If the feature is present and you did not ask for a file on disk, buildx silently rewrites your `docker` export into an `oci` export, because the OCI importer handles multi-platform images. If it is absent, and you asked for more than one platform or for any attestation, it refuses, and the wording depends on whether you asked for a file. Neither wording mentions the test it just failed.

Note the second half of that condition. More than one platform is the famous trigger, but attestations trigger it too, on a perfectly ordinary single-platform build. A workflow that adds provenance for supply chain reasons can start failing with a message about manifest lists it never asked for.

| What the job asked for | Classic image store | containerd image store |
| --- | --- | --- |
| One platform, no attestations, `--load` | Loads normally | Loads normally |
| Two or more platforms, `--load` | Refused before the build | Loads, through the OCI importer |
| One platform with provenance or SBOM, `--load` | Refused before the build | Loads, through the OCI importer |
| Any of the above with `--output type=docker,dest=` | Refused, with the wording naming the oci exporter | Refused, because a file output is not importable |

> Branches read in the buildx export setup on 2026-09-20. The file output case is refused on both stores because the swap to the OCI exporter only happens when no output destination was given.

## Confirm the store before you change anything

The check buildx makes is one you can make yourself in a step, and it answers the question directly rather than by inference. Reading the storage driver name alone is not enough, because the name changes between versions and the field buildx actually reads is the driver status list.

Run this on the runner, not locally. A developer machine with Docker Desktop very often has the containerd store on while the CI runner does not, which is exactly why the build works in one place and not the other.

```.github/workflows/ci.yml
- name: Report the image store the way buildx reads it
  run: |
    docker info --format "{{json .DriverStatus}}"
    docker info --format "{{.Driver}}"
```

## Why no recorded run backs this page

A recorded run of this failure would be a recording of our runner configuration. The error is produced by a client-side branch on a value the daemon reports about itself, so a log proves only which setting that particular daemon had, and a reader whose runner is configured differently learns nothing from it. The interesting content is the branch, and the branch is readable in the source without spending a runner minute.

There is also nothing to repair here. The build never started, no registry was contacted, and the condition is a daemon configuration decision made before the job existed. A recorded run would show a failure that a retry, a bigger runner and a warm cache all leave exactly where it is.

## FAQ

### Why does --push work when --load fails?

Because a push sends the manifest list to a registry, which has always been able to hold one. The refusal is about keeping a manifest list in the local image store, which the classic store cannot do. Nothing about the registry, the credentials or the network is involved in this failure, which is why changing any of them has no effect.

### How does buildx decide the store is containerd?

It reads the daemon info and looks for a driver status entry whose key is `driver-type` and whose value is `io.containerd.snapshotter.v1`. That is the whole test. You can run the same query in a step with a JSON format string, which is more reliable than reading the storage driver name, since that name has changed between versions.

### Does enabling the containerd image store lose my existing images?

The two stores keep separate content, so images built under one are not visible under the other. On an ephemeral CI runner that does not matter, because nothing is there to lose. On a long-lived self-hosted runner it does, and a warm cache of base images will appear to vanish until they are pulled again.

### Why did this start failing when I added provenance?

Because buildx treats any attestation the same way it treats a second platform. Attestations need a format the docker exporter cannot write, so the same branch refuses the build, with a message about manifest lists that does not mention attestations at all. Turning provenance off on the load step restores the previous behavior.

## References

- [buildx: the export setup that swaps or refuses the docker exporter](https://github.com/docker/buildx/blob/master/build/opt.go)
- [buildx: the driver-type test behind the OCI importer feature](https://github.com/docker/buildx/blob/master/util/dockerutil/features.go)
- [BuildKit: the exporter that emits the same sentence server side](https://github.com/moby/buildkit/blob/master/exporter/oci/export.go)
- [Docker docs: the containerd image store](https://docs.docker.com/engine/storage/containerd/)
- [Docker docs: multi-platform builds](https://docs.docker.com/build/building/multi-platform/)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
