PS docker: filter, format & debug containers in 2026

By Kate Baker
PS docker: filter, format & debug containers in 2026

docker ps is usually the first command you type after docker run and the first one you type when something breaks. Most people stop at the default table, squint at a truncated COMMAND column, and then scroll through 30 containers looking for the one that is restarting.

This guide covers the part of docker ps that most tutorials skip: the --filter and --format flags. With them, ps docker output goes from a wall of text to a precise answer to questions like "which containers are unhealthy?", "what is listening on port 5432?", or "which service in my Compose project exited with code 137?". Everything here works with current Docker Engine and the Compose v2 plugin (docker compose).

The basics in 60 seconds

docker ps lists running containers. It is an alias for docker container ls, so both work identically.

docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED        STATUS                  PORTS                    NAMES
3f2a9c1b7d4e   postgres:18   "docker-entrypoint.s…"   2 hours ago    Up 2 hours (healthy)    0.0.0.0:5432->5432/tcp   api-db-1
8b1e0d6f2c3a   api-app       "node server.js"         5 minutes ago  Restarting (1) 3s ago                           api-app-1

The flags you will use every day:

FlagWhat it does
-a, --allInclude stopped, exited, and created containers
-q, --quietPrint only container IDs (for piping into other commands)
-l, --latestShow only the most recently created container
-n 5, --last 5Show the last 5 created containers (includes stopped ones)
-s, --sizeAdd a size column: writable layer size and virtual size
--no-truncDo not truncate IDs or commands
-f, --filterFilter by condition (covered below)
--formatCustomize output with Go templates or JSON

Two habits that save time:

  • When a container "disappeared", run docker ps -a. It almost always exited rather than vanished.
  • When you need the full startup command, use docker ps --no-trunc instead of guessing from "docker-entrypoint.s…".

Filtering: ask a precise question

--filter (or -f) takes key=value pairs. You can pass it multiple times.

Rule to remember: multiple filters with the same key are combined with OR. Filters with different keys are combined with AND.

So -f status=exited -f status=dead means "exited or dead", while -f status=exited -f ancestor=api-app means "exited and built from api-app".

Filter by status

Valid values are created, restarting, running, removing, paused, exited, and dead.

# Everything that is crash-looping right now
docker ps --filter status=restarting

# Everything that has stopped (note: needs -a for non-running states to make sense)
docker ps -a --filter status=exited

Filter by exit code

exited filters by the exit code and only makes sense with -a:

# Killed by SIGKILL, often the OOM killer or a hard stop
docker ps -a --filter exited=137

# Generic application errors
docker ps -a --filter exited=1

Exit code 137 is 128 + 9 (SIGKILL). 143 is 128 + 15 (SIGTERM), which usually means a normal stop that your app handled. If you see 137 on containers you did not stop yourself, check memory limits (more on that in the debugging section).

Filter by health

If your image or Compose file defines a HEALTHCHECK, you can filter on its state: starting, healthy, unhealthy, or none.

docker ps --filter health=unhealthy

This is the fastest way to answer "is the database ready yet?" during local setup.

Filter by name, ID, and label

# Substring match: matches api-db-1, billing-db-1, db-backup...
docker ps --filter name=db

# Anchor it for an exact match
docker ps --filter "name=^api-db-1$"

# Containers with a label (any value)
docker ps --filter label=com.docker.compose.project

# Containers with a specific label value
docker ps --filter label=com.docker.compose.service=worker

Name filters are partial matches by default, which surprises people. Use ^ and $ when you need to target exactly one container in a script.

Labels are the most useful filter once you run more than a handful of containers. Docker Compose sets labels such as com.docker.compose.project and com.docker.compose.service on every container it creates, so you can slice by project or service without depending on naming conventions.

Filter by image

# Containers created from any tag of postgres
docker ps --filter ancestor=postgres

# A specific tag
docker ps --filter ancestor=postgres:18

ancestor also matches containers built from images that use the given image as a base, which is handy when you need to find everything still running on an old base image before an upgrade.

Filter by port, network, and volume

# What is publishing host port 5432?
docker ps --filter publish=5432

# Which containers expose 3000 internally?
docker ps --filter expose=3000

# Everything on a given network
docker ps --filter network=api_default

# Everything mounting a given volume
docker ps -a --filter volume=api_pgdata

publish=5432 is the answer to the classic "port is already allocated" error. Run it, find the container holding the port, and stop it.

Filter by creation order

# Containers created after (or before) a reference container
docker ps -a --filter since=api-db-1
docker ps -a --filter before=api-db-1

Formatting: get exactly the columns you need

--format accepts a Go template. Prefix it with table to get aligned columns with headers.

docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES       STATUS                  PORTS
api-db-1    Up 2 hours (healthy)    0.0.0.0:5432->5432/tcp
api-app-1   Restarting (1) 3s ago

The placeholders available for containers:

PlaceholderMeaning
.IDContainer ID
.ImageImage name
.CommandQuoted command
.CreatedAtCreation timestamp
.RunningForTime since creation, human readable
.PortsPublished ports
.StateShort state, such as running or exited
.StatusStatus with duration and health, such as Up 2 hours (healthy)
.SizeDisk size (requires -s to be meaningful)
.NamesContainer names
.LabelsAll labels as a comma-separated string
.Label "key"Value of one label
.MountsVolume names mounted
.NetworksAttached networks

Useful format recipes

A compact status board:

docker ps -a --format "table {{.Names}}\t{{.State}}\t{{.RunningFor}}\t{{.Image}}"

Group by Compose service:

docker ps --format 'table {{.Label "com.docker.compose.service"}}\t{{.Names}}\t{{.Status}}'

Plain lines for scripting (no table prefix means no header):

docker ps --format "{{.Names}} {{.State}}"

Show networks and mounts when you are debugging "why can't my app reach the database":

docker ps --format "table {{.Names}}\t{{.Networks}}\t{{.Mounts}}"

JSON output for scripts

--format json prints one JSON object per container, one per line. Combine it with jq:

# Names of all unhealthy containers
docker ps --filter health=unhealthy --format json | jq -r '.Names'

# Name and ports for everything running, as TSV
docker ps --format json | jq -r '[.Names, .Ports] | @tsv'

You can also emit a single field as JSON inside a template, for example {{json .Labels}}, but --format json is simpler for most scripts.

Make your favorite format the default

If you always want the same columns, set psFormat in ~/.docker/config.json:

{
  "psFormat": "table {{.Names}}\t{{.State}}\t{{.Status}}\t{{.Ports}}"
}

Plain docker ps will now use that layout. An explicit --format still overrides it. If config.json already exists, merge the key into the existing object instead of overwriting the file, since it may already hold credential and plugin settings.

Using ps docker to debug containers

docker ps does not tell you why something broke, but it tells you where to look. Here is the workflow I use when a stack misbehaves.

Step 1: find the container that is not healthy

docker ps -a --format "table {{.Names}}\t{{.State}}\t{{.Status}}" \
  --filter status=exited --filter status=restarting --filter status=dead

Because the three filters share a key, this shows anything exited, restarting, or dead.

Step 2: read the last logs

docker logs --tail 50 api-app-1

For a crash loop, add --timestamps to see how quickly it restarts. Most crash loops are a missing environment variable, a wrong database host (localhost instead of the service name), or a failed migration.

Step 3: check exit code and OOM

docker inspect --format '{{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}' api-app-1

If OOMKilled=true, raise the memory limit or fix the leak. If the exit code is 137 but OOMKilled=false, something sent SIGKILL, often a stop timeout that expired because the app ignored SIGTERM.

Step 4: check the health check itself

For unhealthy containers, the health log usually explains the failure:

docker inspect --format '{{json .State.Health}}' api-db-1 | jq

A health check that runs curl in an image without curl installed will report unhealthy forever while the app is fine.

Step 5: check resource usage for running containers

docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

docker stats accepts the same table style templates, so the muscle memory transfers.

Docker compose ps

In a Compose project, docker compose ps scopes the list to that project and shows service names:

docker compose ps
docker compose ps -a                         # include stopped services
docker compose ps --status running
docker compose ps --services                 # just the service names
docker compose ps --format json | jq -r '.Service + " " + .State'

--status accepts the same states as docker ps --filter status=. In CI, a quick sanity check after docker compose up -d --wait is:

docker compose ps --status exited --quiet | grep -q . && echo "a service exited" && exit 1

--wait makes up block until services are running and healthy, so the check above catches services that start and then die immediately.

Cleaning up with ps and filters

The -q flag turns any filter into a list of IDs for other commands:

# Stop every container from one Compose service across projects
docker stop $(docker ps -q --filter label=com.docker.compose.service=worker)

# Remove all exited containers created from an old image
docker rm $(docker ps -aq --filter status=exited --filter ancestor=api-app:old)

For general cleanup, docker container prune removes all stopped containers in one go. Both rm and prune permanently delete the containers (not the volumes), so run the docker ps version first to see what will go.

Limitations worth knowing

  • docker ps only sees the Docker daemon it talks to. If you use Docker contexts, check docker context ls, since you may be listing a different machine than you think.
  • .Size without -s is empty, and -s can be slow on hosts with many containers because it calculates disk usage.
  • Filters are evaluated by the daemon, so unsupported keys return an error rather than being silently ignored. That is a feature: typos fail loudly.
  • Status strings like Up 2 hours (healthy) are for humans. In scripts, use .State or --format json instead of parsing .Status.

When you stop needing docker ps in production

On your laptop and on a self-managed VM, docker ps is the ground truth, and the filters above make it fast. In production, though, SSH-ing into a box to run docker ps -a --filter exited=137 means you are also the person responsible for that box: its disk, its restarts, its updates.

If you would rather not, a managed platform like Granite.so runs your containers from a Git repo or Docker image, rebuilds on every push, and handles restarts and scaling for you, with managed databases and S3-compatible storage alongside. Your local docker ps workflow stays exactly the same for development; production just stops being a server you have to babysit. The Hobby plan starts at $9 per month and includes $15 of monthly usage credits.

Quick reference

docker ps -a                                        # everything, including stopped
docker ps -q                                        # IDs only
docker ps --filter status=restarting                # crash loops
docker ps -a --filter exited=137                    # killed containers
docker ps --filter health=unhealthy                 # failing health checks
docker ps --filter "name=^api-db-1$"                # exact name
docker ps --filter publish=5432                     # who owns this port
docker ps --filter label=com.docker.compose.service=web
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
docker ps --format json | jq -r '.Names'
docker compose ps --status running

Keep this block handy, and the next time something breaks you will know which container to blame in one command instead of ten.

Keep reading

More posts tagged “Tutorials”.
Be first in line for updates
and special pricing
Get early access to new features and exclusive discounts delivered straight to your inbox