Use annotations instead of labels for Compose-managed container runtime state
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 38.2k
- Forks
- 5.8k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 55
Description
Context
Spun off from https://github.com/docker/compose/pull/13959#issuecomment-5296188206 (@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 commiton 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 commiton 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/downmatching, 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 adocker commit. - Identity stays as labels (for now):
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:
- 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 (
uprecreates,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/volumeand 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by auditing Compose's label writers and readers, especially the ContainerList filtering and lookup paths listed in the issue, then verify the Engine API's annotation support and compatibility constraints. Done means a defined migration plan that preserves identity-label filtering, supports dual-write and fallback reads, and resolves or explicitly tracks the annotation-filter blocker.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, go
- Domain
- devops, infrastructure
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100