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.

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=key matches 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
docker ps --filter annotation on Docker 29.9.1 and the invalid filter error on 29.7.2
Real output: the annotation filter on Docker 29.9.1 and the error on 29.7.2.

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.

CommandMessageCause / fix
--filter annotations=...invalid filter 'annotations'The key is singular: annotation.
--filter annotationbad format of filter (expected name=value)Give at least a key: annotation=com.example.team.
--filter annotation=... on 29.7.xinvalid 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 =xthe empty string is not permitted as an annotation keyThe 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=KEY and annotation=KEY=VALUE need 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 prune does not take the annotation filter.
  • On older engines, use docker inspect with .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

Popular posts from this blog

How to Play Minecraft Bedrock Edition on Linux: A Comprehensive Guide for Tech Professionals

The Ultimate Guide: How to Set Up DXVK in Wine on Linux for Enhanced Gaming Performance

Linux desktop frozen? My 5 go-to tricks to try - before forcing a hard reboot