applebee.io logo
Back to articles
DockerDevOpsWorkflow

Stop Reading Docker IDs: Make docker ps Useful Again

Jim Applebee

6 min read

One of the first things I change on a new development machine is not my editor theme, my IDE, or my shell prompt.

It is docker ps.

That sounds too small to matter. It is not.

The longer you operate real systems, the more you realize that productivity is rarely limited by typing speed. It is limited by cognitive overhead. Every unnecessary column, repeated decision, extra click, and needless context switch spends attention you may need five minutes later.

Attention is one of the most valuable resources an engineer has.

So I tune the tools I use hundreds of times a week. Not to make them prettier. To make them stop asking me to discard irrelevant information before I can think.

Docker is a good example.

The default problem

The default docker ps output is designed for completeness. It gives you everything Docker thinks might matter:

CONTAINER ID   IMAGE                 COMMAND                  CREATED          STATUS                    PORTS                                      NAMES
8f1c7d0a2d31   postgres:16           "docker-entrypoint.s…"   3 hours ago      Up 3 hours (healthy)      0.0.0.0:5432->5432/tcp                     billing-db-1
4c02a88aa7f4   redis:7               "docker-entrypoint.s…"   3 hours ago      Up 3 hours                0.0.0.0:6379->6379/tcp                     billing-cache-1
9b42df40ddfb   billing-api:latest    "node server.js"         3 hours ago      Up 3 hours                0.0.0.0:3000->3000/tcp                     billing-api-1

That is not bad output. It is just optimized for Docker’s general contract, not for the way an engineer usually reads a running system.

Most of the time I am asking:

  • What is running?
  • Is it healthy?
  • Where is it listening?

I almost never care about the container ID during normal work. If I am looking for a container ID, something unusual has already happened. The same is true for the full command string and the exact creation timestamp. Those fields matter sometimes, but they do not deserve prime real estate every time I glance at the system.

The goal is not to see more information. The goal is to recognize the pattern sooner.

That distinction matters in enterprise systems. Architecture is not only diagrams, standards, and platform choices. A lot of architecture is reducing friction between people, systems, tooling, and operations so the right decision becomes easier to make under pressure.

Optimize your tools for the common case

Software ships with defaults. Professionals rarely keep all of them.

Defaults are designed for everyone. Workflows are designed by the people who actually do the work.

That is why experienced engineers tune their IDEs, keyboard shortcuts, Git aliases, shell prompts, terminal themes, tmux layouts, VS Code settings, IntelliJ keymaps, and logging views. None of those changes are impressive by themselves. Together, they create an environment where the common case is clear, repeatable, and hard to get wrong.

Docker should be treated the same way.

I optimize for the hundreds of times I need to understand running containers, not for the one time I need every available field. When I need more detail, I can still ask Docker for more detail. But I do not want the rare case to dominate the daily view.

Good tools do not make engineers faster by magic. They remove unnecessary thinking from the path between observation and judgment.

Set a global psFormat

Docker lets you set a default psFormat in ~/.docker/config.json. I reduce docker ps to the three fields I care about during normal work:

  • container name
  • status
  • ports

Use this to set it globally:

python3 - <<'PY'
import json
from pathlib import Path

p = Path.home() / ".docker" / "config.json"
p.parent.mkdir(exist_ok=True)

data = json.loads(p.read_text()) if p.exists() else {}
data["psFormat"] = "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
p.write_text(json.dumps(data, indent=2) + "\n")
PY

Now docker ps becomes closer to an operator’s view:

NAMES             STATUS                 PORTS
billing-db-1      Up 3 hours (healthy)   0.0.0.0:5432->5432/tcp
billing-cache-1   Up 3 hours             0.0.0.0:6379->6379/tcp
billing-api-1     Up 3 hours             0.0.0.0:3000->3000/tcp

That table answers the question I usually have.

Name tells me what I am looking at. Status tells me whether it is alive and healthy. Ports tell me how it is connected to the host. That is enough for the first pass, and the first pass is where good troubleshooting starts.

If I need the full view, I can still ask for it:

docker ps --no-trunc
docker ps --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}'

Customization should not remove capability. It should make the normal path sharper and leave the uncommon path available.

I have spent too many late nights on production issues where the first few minutes were lost to basic orientation: which node, which service, which environment, which process, which owner. You do not get those minutes back. Small improvements that shorten orientation are not cosmetic; they change how quickly a team can move from confusion to action.

Names are operational language

Cleaner output only helps if names carry meaning.

Container names are not just labels. They become part of the team’s operational language:

  • “Check the API logs.”
  • “Restart Postgres.”
  • “Exec into Redis.”
  • “Is the worker running?”

Good names reduce translation. Bad names add a tax to every conversation, every incident, and every handoff.

For one-off containers, name them:

docker run --name local-redis -p 6379:6379 redis:7

Then use the name everywhere:

docker logs -f local-redis
docker stop local-redis
docker rm local-redis

For Compose projects, set a clear project name when it matters:

COMPOSE_PROJECT_NAME=billing docker compose up -d

That gives you names like billing-api-1, billing-db-1, and billing-cache-1.

Those names communicate system shape. They tell you what belongs together. They help a developer, architect, or operator understand the application boundary without opening another file.

I avoid hard-coding container_name in Compose files unless there is a specific operational reason. It can make scaling and reuse harder. A good Compose project name usually gives you readability without taking away flexibility.

That distinction matters. Architecture is often the art of improving clarity without creating unnecessary coupling.

Filter before you scan

Once the default columns are useful, filters become more valuable.

Show running containers for one project:

docker ps --filter "name=billing"

Show containers with health checks that are not healthy:

docker ps --filter "health=unhealthy"

Show exited containers when something disappeared:

docker ps -a --filter "status=exited"

This is not about memorizing Docker flags. It is about narrowing the problem space before your eyes start scanning.

Experienced engineers do this constantly. They reduce scope. They remove noise. They create a smaller, more accurate view of the system before making a decision.

That habit matters whether you are looking at containers, logs, metrics, traces, cloud resources, firewall rules, or a database query plan. The tool changes. The discipline does not.

Build your developer cockpit

The goal is not a collection of clever aliases. The goal is a repeatable operational cockpit.

I keep aliases boring:

alias dps='docker ps'
alias dpa='docker ps -a'
alias dlf='docker logs -f'

Then the daily workflow becomes predictable:

dps
dlf billing-api-1
docker exec -it billing-api-1 sh
docker compose restart api
docker compose ps

There is nothing sophisticated in that sequence. That is the point.

When a command path is repeated hundreds of times a month, friction compounds. A better view, a predictable name, and a familiar sequence reduce the attention spent operating the tool. That leaves more attention for the actual problem.

Good engineers build these paths intentionally. They do not wait for tooling to become perfect. They shape the edges they touch every day.

At enterprise scale, those edges are where a lot of delivery friction hides. A team rarely loses a week because one command was ugly. It loses a week through hundreds of small pauses, misunderstandings, and rechecks that nobody treats as architecture because they do not appear on a roadmap.

Use Compose when the project is the context

When I am inside a Compose-based project, I prefer Compose commands for project-level work:

docker compose ps
docker compose logs -f api
docker compose restart api
docker compose exec api sh

Plain Docker commands are useful when I am looking across the whole machine. Compose commands are better when I am operating inside one application stack.

That separation prevents ambiguity.

Two projects can both have a service called api. Two projects can both run Postgres. The right command context reduces the chance that you inspect, restart, or remove the wrong thing.

This is the same principle as using the right namespace in Kubernetes, the right subscription in Azure, the right account in AWS, or the right environment in a CI/CD system. Operational context is not ceremony. It is a guardrail.

Clean up deliberately

Cleaner output also makes stale containers easier to see. But cleanup should still be deliberate.

Start with visibility:

docker system df
docker ps -a

Then remove the thing you actually mean to remove:

docker container prune
docker image prune
docker volume prune

I do not treat docker system prune -a --volumes as a casual cleanup command. It has its place, but it is too broad for routine work on a machine with multiple active projects.

That is not Docker caution. That is operational discipline.

The same habit applies everywhere: understand the blast radius before running the command that removes things. A mature engineer is not slower because they are careful. They are faster because they know which mistakes are expensive.

The real improvement

This article started with docker ps, but the point is larger than Docker.

The best productivity improvements are rarely dramatic. They are the small decisions you make once that quietly save attention every day.

Eventually your tools stop asking you to decode them. They begin presenting the information you actually need, in the shape you need it, at the moment you need it.

That is good engineering, and it is good architecture.

Not because it saves a few seconds at the terminal.

Because experienced engineers intentionally shape their environment so their attention is available for the problems that actually deserve it.