This issue was filed by an AI agent on a human's behalf. The human submitter may not have independently verified the report.
Context
Spun off from #13959 (comment) (@thaJeztah):
compose is already heavily depending on labels to store state, but we should start looking if we could transition to using Annotations, especially for state that shouldn't be persisted (doing a docker commit on a container, or running a container from an image that already has labels set could probably still provide "interesting" results)
Problem
Compose stores both identity and runtime state as container labels (com.docker.compose.project, service, container-number, oneoff, config-hash, image, depends_on, replace, version, project.working_dir, project.config_files, environment_file, …). Labels have two persistence problems annotations don't have:
docker commit on a Compose-managed container bakes all of this state into the resulting image;
- containers inherit image labels, so running a container from such an image (or any image shipping
com.docker.compose.* labels) makes it look like Compose-managed state: wrong config-hash comparisons, orphan detection, ps/down matching, etc.
HostConfig.Annotations is per-container, non-identifying runtime metadata — exactly the right home for state that must never leak into images.
Proposal
Split the current label set by role:
- Runtime state → annotations:
config-hash, image, depends_on, replace, container-number, oneoff, version, project.working_dir, project.config_files, environment_file — none of these should survive a docker commit.
- Identity stays as labels (for now):
project and service are used in server-side filters (docker ps --filter label=…, events, networks/volumes listing); the Engine API has no annotation-based filtering, and annotations don't exist on networks/volumes/images at all. Per the original comment, containerd namespaces may eventually be the right primitive for project scoping — that's engine-side work to track separately.
Backward compatibility
Migration must be invisible to users with running stacks:
- Phase 1 (dual write, fallback read): new containers get both labels and annotations; all readers prefer the annotation and fall back to the label. Stacks created by older Compose keep working (
up recreates, down, ps).
- Phase 2 (annotation-first): stop writing the state labels on new containers; keep the fallback read path for at least a major-version window.
- Filters keep using the identity labels throughout; nothing changes for
docker ps --filter label=com.docker.compose.project=… consumers, and third-party tooling relying on identity labels is unaffected.
A container recreated by a newer Compose gets the new layout; mixed projects (old + new containers) must reconcile correctly during phase 1, which the fallback read gives us.
Open questions
- Exact split for
container-number/oneoff: they are identity-adjacent but not used in server-side filters everywhere — audit each call site.
- Annotations are only available for containers: networks, volumes and images keep labels regardless (their divergence-detection labels like
com.docker.compose.network/volume and config-hash stay as-is).
- Whether third-party ecosystems read the state labels (not just identity) — a deprecation note in release notes should cover phase 2.
Known direct label consumers
Compose labels were never a documented API, yet some external tools are known to read com.docker.compose.* labels directly from docker ps output instead of using compose ps or another supported interface — Docker Desktop has been identified as one such consumer. These consumers will need to be updated; the phase 1 dual-write window is their migration period, and does not turn the state labels into a compatibility commitment.
Blocker: the Engine API has no annotation filter
ContainerList returns annotations since Engine v27.0.0 (moby/moby ca0529f984, HostConfig.Annotations in each summary — verified empirically against v29.6.1 / API 1.55), so list-driven reads keep working once state moves to annotations. But server-side filtering by annotation does not exist (invalid filter 'annotation'; label only). This is a blocker for moving any filtered label, and Compose filters on more than identity today:
| Label |
Filter kind |
Server-side usage |
com.docker.compose.project |
equality + presence |
ContainerList (all lookups, ls project discovery), NetworkList, VolumeList, ImageList (pruner, watch), Events (up monitor), stats |
com.docker.compose.service |
equality |
ContainerList (getDefaultFilters), ImageList (watch), stats |
com.docker.compose.oneoff |
equality |
ContainerList (getDefaultFilters, start, monitor initial state), Events (up monitor) |
com.docker.compose.config-hash |
presence ("compose-managed" marker) |
ContainerList: ls, getDefaultFilters (i.e. virtually every container lookup), monitor initial state |
com.docker.compose.container-number |
equality (--index lookup) |
ContainerList (getSpecifiedContainer) |
com.docker.compose.network |
equality |
NetworkList (down) |
Consequence: two labels from the runtime state basket are filtered on today. Migrating them requires either an annotation filter landing engine-side, or Compose first rewriting those filter usages — the config-hash presence marker can be replaced by the project label presence, and the container-number equality by client-side selection over a service-scoped list. Both rewrites are Compose-local and should land as a phase 1 prerequisite; identity labels (project, service, oneoff, network) stay labels regardless until the engine offers annotation filtering.
This issue was filed by an AI agent on a human's behalf. The human submitter may not have independently verified the report.
Context
Spun off from #13959 (comment) (@thaJeztah):
Problem
Compose stores both identity and runtime state as container labels (
com.docker.compose.project,service,container-number,oneoff,config-hash,image,depends_on,replace,version,project.working_dir,project.config_files,environment_file, …). Labels have two persistence problems annotations don't have:docker commiton a Compose-managed container bakes all of this state into the resulting image;com.docker.compose.*labels) makes it look like Compose-managed state: wrong config-hash comparisons, orphan detection,ps/downmatching, etc.HostConfig.Annotationsis per-container, non-identifying runtime metadata — exactly the right home for state that must never leak into images.Proposal
Split the current label set by role:
config-hash,image,depends_on,replace,container-number,oneoff,version,project.working_dir,project.config_files,environment_file— none of these should survive adocker commit.projectandserviceare used in server-side filters (docker ps --filter label=…, events, networks/volumes listing); the Engine API has no annotation-based filtering, and annotations don't exist on networks/volumes/images at all. Per the original comment, containerd namespaces may eventually be the right primitive for project scoping — that's engine-side work to track separately.Backward compatibility
Migration must be invisible to users with running stacks:
uprecreates,down,ps).docker ps --filter label=com.docker.compose.project=…consumers, and third-party tooling relying on identity labels is unaffected.A container recreated by a newer Compose gets the new layout; mixed projects (old + new containers) must reconcile correctly during phase 1, which the fallback read gives us.
Open questions
container-number/oneoff: they are identity-adjacent but not used in server-side filters everywhere — audit each call site.com.docker.compose.network/volumeand config-hash stay as-is).Known direct label consumers
Compose labels were never a documented API, yet some external tools are known to read
com.docker.compose.*labels directly fromdocker psoutput instead of usingcompose psor another supported interface — Docker Desktop has been identified as one such consumer. These consumers will need to be updated; the phase 1 dual-write window is their migration period, and does not turn the state labels into a compatibility commitment.Blocker: the Engine API has no annotation filter
ContainerListreturns annotations since Engine v27.0.0 (moby/moby ca0529f984,HostConfig.Annotationsin each summary — verified empirically against v29.6.1 / API 1.55), so list-driven reads keep working once state moves to annotations. But server-side filtering by annotation does not exist (invalid filter 'annotation';labelonly). This is a blocker for moving any filtered label, and Compose filters on more than identity today:com.docker.compose.projectContainerList(all lookups,lsproject discovery),NetworkList,VolumeList,ImageList(pruner, watch),Events(up monitor),statscom.docker.compose.serviceContainerList(getDefaultFilters),ImageList(watch),statscom.docker.compose.oneoffContainerList(getDefaultFilters,start, monitor initial state),Events(up monitor)com.docker.compose.config-hashContainerList:ls,getDefaultFilters(i.e. virtually every container lookup), monitor initial statecom.docker.compose.container-number--indexlookup)ContainerList(getSpecifiedContainer)com.docker.compose.networkNetworkList(down)Consequence: two labels from the runtime state basket are filtered on today. Migrating them requires either an
annotationfilter landing engine-side, or Compose first rewriting those filter usages — theconfig-hashpresence marker can be replaced by theprojectlabel presence, and thecontainer-numberequality by client-side selection over a service-scoped list. Both rewrites are Compose-local and should land as a phase 1 prerequisite; identity labels (project,service,oneoff,network) stay labels regardless until the engine offers annotation filtering.