Repository navigation
Use annotations instead of labels for Compose-managed container runtime state #14050
Description
Activity
Follow-up after moby/moby#53538 (
annotationfilter forGET /containers/json) merged. That PR only touchesdaemon/list.go— it unblocks theContainerList-based filters in the table above (config-hashpresence,container-numberequality, andproject/service/oneoffif we ever wanted to move those). It does not close the whole gap:1. Annotations only exist on containers.
HostConfig.Annotationshas no equivalent on networks, volumes, or images — and won't via this PR. Concretely:com.docker.compose.project/servicebaked onto built images (getImageBuildLabels, BuildKit/bake path only — the classic-builder path currently skips this entirely, a separate pre-existing gap) stay labels forever; they're baked into the image config and persist throughdocker commit/push/retag/reuse as a base image, regardless of anything done on the container side. The original motivating scenario ("a container inheriting compose state from an image") is only closed for state written by Compose at container-create time — not for identity labels an image carries because Compose built it.com.docker.compose.config-hashis reused across containers, networks, and volumes (create.go:1465,1554,observed_state.go). Migration has to be per call-site/object-type, not per label name — the same constant migrates to annotation for containers but must stay a label for networks/volumes indefinitely.
2. Event filtering isn't covered.
daemon/events/filter.go(matchLabels) only knowslabel, notannotation. Theupreconciliation monitor filtersEventsbyproject/oneoff— that path stays blocked until (if) a separate moby PR adds annotation support there.statsisn't a separate blocker: it resolves its container set viaContainerListfirst, so it rides along with #53538.3. Transition complexity is bigger than "one version window". The annotation filter is brand new server-side; Compose supports a much wider range of engine versions (and other Docker-API-compatible daemons). Phase 2 (stop writing state labels) can't be gated on a Compose version bump — it needs a per-connection capability check (probe the filter / API version), otherwise users on older engines silently lose filtering once labels are dropped. Practically, phase 1 (dual-write, label-fallback read) likely needs to stay in place for as long as those engines are supported — which also means the image-label-inheritance false-positive this issue is meant to fix keeps happening for that whole window, since the fallback read still trusts labels.
@ndeloof
Following up on the phase 1 prerequisites listed above: I'd like to take thecontainer-numberone.getSpecifiedContainer(used byexec,logs,port,cp,attach,commitandexportfor--index) adds a server-sidecontainer-numberlabel filter. The list is already scoped to one service, so I'd drop that filter and pick the replica client-side. That keeps--indexworking on engines without the annotation filter once the label moves. No behavior change.For the
config-hashpresence marker, switching toprojectpresence would make two kinds of containers visible togetDefaultFiltersandlsthat are hidden today:- pre_start hook runners, which are deliberately created without
config-hash; - containers started with plain
docker runfrom an image Compose built, sincegetImageBuildLabelsputsproject/serviceon the image. That is the inheritance problem this issue describes.
The first could be excluded by
HookLabel, but I don't see a way to tell the second apart. Would you rather keepconfig-hashas the membership marker through phase 1?- pre_start hook runners, which are deliberately created without
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.