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 psCONTAINER 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-1The flags you will use every day:
| Flag | What it does |
|---|---|
-a, --all | Include stopped, exited, and created containers |
-q, --quiet | Print only container IDs (for piping into other commands) |
-l, --latest | Show only the most recently created container |
-n 5, --last 5 | Show the last 5 created containers (includes stopped ones) |
-s, --size | Add a size column: writable layer size and virtual size |
--no-trunc | Do not truncate IDs or commands |
-f, --filter | Filter by condition (covered below) |
--format | Customize 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-truncinstead 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=exitedFilter 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=1Exit 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=unhealthyThis 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=workerName 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:18ancestor 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_pgdatapublish=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-1Formatting: 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 agoThe placeholders available for containers:
| Placeholder | Meaning |
|---|---|
.ID | Container ID |
.Image | Image name |
.Command | Quoted command |
.CreatedAt | Creation timestamp |
.RunningFor | Time since creation, human readable |
.Ports | Published ports |
.State | Short state, such as running or exited |
.Status | Status with duration and health, such as Up 2 hours (healthy) |
.Size | Disk size (requires -s to be meaningful) |
.Names | Container names |
.Labels | All labels as a comma-separated string |
.Label "key" | Value of one label |
.Mounts | Volume names mounted |
.Networks | Attached 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=deadBecause 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-1For 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-1If 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 | jqA 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 psonly sees the Docker daemon it talks to. If you use Docker contexts, checkdocker context ls, since you may be listing a different machine than you think..Sizewithout-sis empty, and-scan 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.Stateor--format jsoninstead 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 runningKeep this block handy, and the next time something breaks you will know which container to blame in one command instead of ten.