Skip to content

[BUG] Second up recreates image pulled by first up with containerd store #14036

Description

@lancefrench

This issue was filed by an AI agent on a human's behalf. The human submitter may not have independently verified the report.

Description

On an Engine using the containerd image store, Compose 5.4.0 can recreate a container on the second identical docker compose up when the first up had to pull a multi-manifest image.

Current behavior:

  • The first up pulls the missing image and creates the container.
  • The second identical up, without another pull or any configuration/image change, reports the service as Recreate and replaces the container.

Expected behavior:

  • The second up recognizes that the image and configuration are unchanged and leaves the container running.

This can destroy state held in the container writable layer. We encountered it with a stateful infrastructure container initialized between the two Compose invocations.

Steps To Reproduce

Use Docker Engine with the containerd image store and API >= 1.48. Start with the selected multi-platform image absent locally.

services:
  app:
    image: alpine:3.22.2
    command: ["sleep", "infinity"]
docker image rm alpine:3.22.2 # only to make the first Compose invocation pull

docker compose up -d
first_id=$(docker compose ps -q app)
docker inspect -f '{{index .Config.Labels "com.docker.compose.image"}}' "$first_id"

docker compose up -d
second_id=$(docker compose ps -q app)

test "$first_id" = "$second_id"

On the affected setup, the second up prints Recreate and the final comparison fails.

Compose Version

Docker Compose version v5.4.0

Docker Environment

Docker Engine 29.7.2
API version 1.55
Storage Driver: overlayfs
driver-type: io.containerd.snapshotter.v1
containerd 2.3.3
linux/amd64

Anything else?

This appears to be a digest-type inconsistency introduced when getImageSummaries was changed by fd794ea to select the platform runnable-manifest digest:

  • During the initial pull, pullRequiredImages stores the value returned by pullServiceImage in the project image map.
  • pullServiceImage currently returns plain ImageInspect.ID. With the containerd image store, this can be the top-level image-index digest. That value becomes the new container's com.docker.compose.image label.
  • During the next up, the image is already local. getImageSummaries requests manifests and contentDigest returns the platform runnable-manifest digest.
  • Reconciliation compares the runnable-manifest digest with the existing container's index-digest label and treats the unchanged image as stale.

In the observed failure, the existing container label held sha256:46302bcb91a7e8008e6394be8afafdbfa40fb77a54d4046a38be35992042d5de, while the later local-image lookup selected a different platform manifest digest for the same unchanged tag. The Compose configuration hash and mounts were identical, and no pull occurred during the second invocation.

Compose 5.1.4 does not reproduce this because both paths use plain inspect.ID. A patch that resolves the post-pull image through the same getImageSummaries path fixes the mismatch.

This is related to the containerd multi-manifest handling in #14007, but it is a different path and symptom: #14007 rejects a locally available non-native platform and attempts a pull; this issue successfully pulls a same-platform image and then unnecessarily recreates it on the following up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions