docker ps --filter annotation: 9 Easy Examples and Errors
Docker Engine 29.8.0 added an annotation filter to the container list. Run docker ps --filter annotation=KEY to list containers that carry an annotation key, or docker ps --filter annotation=KEY=VALUE to match the key and the exact value. Older engines (29.7.x and before) store annotations set with docker run --annotation, but answer the same filter with Error response from daemon: invalid filter 'annotation'.
Every example below was tested on 2026-10-20 with the official images docker:29.9.1-dind (API 1.56) and docker:29.7.2-dind (API 1.55), each running in a rootless podman container. All output is copied from that lab; nothing is shortened except long container IDs.
Contents
- What changed in Docker 29.8.0
- Annotations vs labels
- docker ps --filter annotation: key and key=value
- Exact match only: no wildcards, case-sensitive
- Empty values
- Combining docker ps --filter annotation with other filters
- The label filter does not see annotations
- Use the filter with stop, inspect and prune
- The annotation filter in the Engine API
- Errors and fixes
- Docker 29.7 and older: the inspect workaround
- Summary
What changed in Docker 29.8.0
The Docker Engine 29.8.0 release notes (2026-09-03) list: "Add annotation filter to container listings (docker ps, GET /containers/json) allowing to filter containers by their annotations." The change is moby pull request 53538. The Engine API v1.56.0 notes describe the syntax:
annotation=keymatches containers that have the annotation key.annotation="key=value"matches containers where that key has that value.- It works "similar to the existing label filter".
On 2026-10-20 the docker container ls reference did not list the new key in its filter table yet, so the release notes are the main source. The docker run reference describes --annotation as "Add an annotation to the container (passed through to the OCI runtime)", available since API 1.43.
Annotations vs labels
Labels are Docker metadata: docker ps shows them and many tools read them. Annotations are OCI metadata: Docker stores them in HostConfig.Annotations and writes them into the OCI runtime spec of the container. Until 29.8.0 there was no way to search for them with docker ps. The lab starts six containers on the 29.9.1 engine:
$ docker run -d --name pay-api --annotation com.example.team=payments --annotation com.example.tier=gold alpine:3.22 sleep 1d $ docker run -d --name pay-worker --annotation com.example.team=payments --annotation com.example.tier=silver alpine:3.22 sleep 1d $ docker run -d --name web --annotation com.example.team=frontend alpine:3.22 sleep 1d $ docker run -d --name labelled --label com.example.team=payments alpine:3.22 sleep 1d $ docker run -d --name empty --annotation com.example.debug= alpine:3.22 sleep 1d $ docker run --name done --annotation com.example.team=payments alpine:3.22 true
The LABELS column only shows the real label. Annotations are not visible in the list:
$ docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Labels}}' NAMES STATUS LABELS done Exited (0) Less than a second ago empty Up Less than a second labelled Up Less than a second com.example.team=payments web Up 1 second pay-worker Up 1 second pay-api Up 1 second $ docker inspect -f '{{.Name}} {{json .HostConfig.Annotations}}' pay-api web labelled empty /pay-api {"com.example.team":"payments","com.example.tier":"gold"} /web {"com.example.team":"frontend"} /labelled null /empty {"com.example.debug":""}
The annotations also reach the OCI runtime. Inside the dind container, the bundle that containerd hands to runc has them in config.json:
$ id=$(docker inspect -f '{{.Id}}' pay-api); jq -c '.annotations' /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/$id/config.json {"com.example.team":"payments","com.example.tier":"gold"}
docker ps --filter annotation: key and key=value
The docker ps --filter annotation syntax has two forms. With only a key, every running container that has that key matches, whatever the value:
$ docker ps --filter annotation=com.example.team --format '{{.Names}}' web pay-worker pay-api
With key=value, only an exact value matches. Add -a to include stopped containers, as with every other docker ps filter:
$ docker ps --filter annotation=com.example.team=payments --format '{{.Names}}' pay-worker pay-api $ docker ps -a --filter annotation=com.example.team=payments --format '{{.Names}} {{.Status}}' done Exited (0) Less than a second ago pay-worker Up 1 second pay-api Up 2 seconds $ docker ps --filter annotation=com.example.tier=gold --format '{{.Names}}' pay-api

Exact match only: no wildcards, case-sensitive
The value comparison is exact. A different case or a * wildcard returns nothing, and there is no error to warn you:
$ docker ps --filter annotation=com.example.team=Payments --format '{{.Names}}' $ docker ps --filter 'annotation=com.example.team=pay*' --format '{{.Names}}'
If you need a prefix or regex match, list by key and filter the output yourself, for example with docker inspect and grep as shown in the last section.
Empty values
An annotation can have an empty value. The key-only filter finds it, and key= (equals sign with nothing after it) matches only the empty value:
$ docker ps --filter annotation=com.example.debug --format '{{.Names}}' empty $ docker ps --filter annotation=com.example.debug= --format '{{.Names}}' empty
In the error test further down, a container started with --annotation com.example.team (no equals sign) got the value "". annotation=com.example.team= then listed only that container, not web or pay-api, which have real values.
Combining docker ps --filter annotation with other filters
Several --filter flags are combined with AND. Two different annotation keys narrow the list:
$ docker ps --filter annotation=com.example.team=payments --filter annotation=com.example.tier=gold --format '{{.Names}}' pay-api
Two values for the same key do not work as OR. No container can have tier=gold and tier=silver at once, so the result is empty:
$ docker ps --filter annotation=com.example.tier=gold --filter annotation=com.example.tier=silver --format '{{.Names}}'
Run two commands if you need both values. Mixing docker ps --filter annotation with name works as expected:
$ docker ps --filter annotation=com.example.team=payments --filter name=worker --format '{{.Names}}' pay-worker
The label filter does not see annotations
The same key and value set as a label and as an annotation are two different things. The label filter finds only the labelled container, and the annotation filter never returns it. Both filters together return nothing, because no container has both:
$ docker ps --filter label=com.example.team=payments --format '{{.Names}}' labelled $ docker ps --filter annotation=com.example.team --format '{{.Names}}' | grep -c labelled 0 $ docker ps --filter annotation=com.example.team=payments --filter label=com.example.team=payments --format '{{.Names}}'
So if you move metadata from labels to annotations, update every script that uses --filter label=.
Use the filter with stop, inspect and prune
The usual pattern of docker ps -q piped into another command works with the new filter. Stopping all silver containers:
$ docker ps -q --filter annotation=com.example.tier=silver | xargs docker stop 4f026f3b8922 $ docker ps -a --filter annotation=com.example.team=payments --format '{{.Names}} {{.Status}}' done Exited (0) 12 seconds ago pay-worker Exited (137) Less than a second ago pay-api Up 13 seconds
docker container prune does not accept the new key in 29.9.1. It still accepts label:
$ docker container prune -f --filter annotation=com.example.team=payments Error response from daemon: invalid filter 'annotation' $ docker container prune -f --filter label=com.example.team=payments Total reclaimed space: 0B
To remove stopped containers by annotation, list them with docker ps -a -q --filter status=exited --filter annotation=... and pass the IDs to docker rm. The {{json .}} format of docker ps has no annotations field either, so use docker inspect to print the values:
$ docker ps --format '{{json .}}' --filter name=pay-api | tr ',' '\n' | head -20 {"Command":"\"sleep 1d\"" "CreatedAt":"2026-10-20 08:41:15 +0000 UTC" "HealthStatus":"none" "ID":"e8f3222e7279" "Image":"alpine:3.22" "Labels":"" "LocalVolumes":"0" "Mounts":"" "Names":"pay-api" "Networks":"bridge" "Platform":null "Ports":"" "RunningFor":"14 seconds ago" "Size":"0B (virtual 8.29MB)" "State":"running" "Status":"Up 14 seconds"}
The annotation filter in the Engine API
Scripts that talk to the Docker socket send the filter as JSON in the filters query parameter of GET /containers/json. Only running containers are returned unless you add all=1, which is why pay-worker (stopped above) is missing:
$ curl -s --unix-socket /var/run/docker.sock -G http://localhost/v1.56/containers/json --data-urlencode 'filters={"annotation":["com.example.team=payments"]}' | jq -r '.[].Names[0]' /pay-api
In this lab a 29.9.1 daemon also applied the filter when the request used the older /v1.55/ path, so the engine version matters more than the API version in the URL:
$ curl -s --unix-socket /var/run/docker.sock -G http://localhost/v1.55/containers/json --data-urlencode 'filters={"annotation":["com.example.team=payments"]}' | jq -c 'if type=="array" then [.[].Names[0]] else . end' ["/pay-api"] $ curl -s --unix-socket /var/run/docker.sock -G http://localhost/v1.56/containers/json --data-urlencode 'filters={"annotation":["com.example.team=payments"]}' | jq -c '.[0] | keys' ["Command","Created","Health","HostConfig","Id","Image","ImageID","Labels","Mounts","Names","NetworkSettings","Ports","State","Status"]
As with the CLI, the list response has no top-level Annotations key; call GET /containers/{id}/json to read the values.
Errors and fixes
These are the messages seen in the lab, with the cause of each.
| Command | Message | Cause / fix |
|---|---|---|
--filter annotations=... | invalid filter 'annotations' | The key is singular: annotation. |
--filter annotation | bad format of filter (expected name=value) | Give at least a key: annotation=com.example.team. |
--filter annotation=... on 29.7.x | invalid filter 'annotation' | The engine is older than 29.8.0. Upgrade, or use the inspect workaround below. |
container prune --filter annotation=... | invalid filter 'annotation' | Prune does not support it; use docker ps -a -q + docker rm. |
run --annotation =x | the empty string is not permitted as an annotation key | The key before = must not be empty. |
The full output of the two filter mistakes:
$ docker ps --filter annotations=com.example.team Error response from daemon: invalid filter 'annotations' $ docker ps --filter annotation invalid argument "annotation" for "-f, --filter" flag: bad format of filter (expected name=value) Usage: docker ps [OPTIONS] Run 'docker ps --help' for more information
And the two --annotation cases on docker run. A key without = is accepted and stored with an empty value; an empty key is refused by the daemon:
$ docker run -d --name nokey --annotation com.example.team alpine:3.22 sleep 1d >/dev/null && docker inspect -f '{{json .HostConfig.Annotations}}' nokey {"com.example.team":""} $ docker ps --filter annotation=com.example.team= --format '{{.Names}}' nokey $ docker run -d --annotation =x alpine:3.22 true docker: Error response from daemon: invalid Annotations: the empty string is not permitted as an annotation key Run 'docker run --help' for more information
Docker 29.7 and older: the inspect workaround
On 29.7.2 the annotation is stored exactly as on the new engine, but the docker ps --filter annotation call is rejected by the daemon:
$ docker version --format 'client {{.Client.Version}} / server {{.Server.Version}} / API {{.Server.APIVersion}}' client 29.7.2 / server 29.7.2 / API 1.55 $ docker inspect -f '{{json .HostConfig.Annotations}}' pay-api {"com.example.team":"payments"} $ docker ps --filter annotation=com.example.team=payments Error response from daemon: invalid filter 'annotation'
Because the error comes from the daemon, it is the Docker Engine (server) version that has to be 29.8.0 or newer; check it with docker version. Until you can upgrade, read the annotations with docker inspect and filter with grep:
$ docker inspect -f '{{.Name}} {{index .HostConfig.Annotations "com.example.team"}}' $(docker ps -q) /pay-api payments $ docker ps -q | xargs docker inspect --format '{{.Name}} {{json .HostConfig.Annotations}}' | grep '"com.example.team":"payments"' /pay-api {"com.example.team":"payments"}
This is slower on hosts with many containers, since it inspects each one, but it works on any engine that supports --annotation.
Summary
docker ps --filter annotation=KEYandannotation=KEY=VALUEneed Docker Engine 29.8.0 or newer (API 1.56).- Matching is exact and case-sensitive;
KEY=matches only empty values. - Multiple filters are combined with AND, also for two values of the same key.
- Labels and annotations are separate;
docker container prunedoes not take the annotation filter. - On older engines, use
docker inspectwith.HostConfig.Annotations.
For more everyday commands, see top Docker commands for DevOps engineers. If a container you filtered does not behave, the guide to debugging Docker containers covers logs, exec and inspect, and this post shows how to get a container's IP address with the same docker inspect templates.
Comments
Post a Comment