Debugging Containers From the Terminal: A Practical Docker CLI Workflow

A crashing container pushes you toward the dramatic move — restart, redeploy, rebuild — before you know what's wrong. Most of the time the answer is one cheap command away. This fixed-order sequence starts with the cheapest check and moves up layers. Use it every time.

Step 1: docker ps -a — Status and Exit Code

First question: what state is the container in? Not what you think — what Docker says.

docker ps -a

The -a flag is mandatory. A crashed container doesn't appear in plain docker ps. Look at the STATUS column:

Exit codes decode quickly:

137 vs 143 matters. 143 is usually expected. 137 is worth investigating.

Step 2: docker logs --tail then -f — What Did It Say?

Read what the container actually said. Start bounded:

docker logs --tail 100 web-1
docker logs --tail 100 -t web-1   # with timestamps
docker logs -f web-1              # follow live

For a crash loop, --tail captures the death rattle. If the container restarts fast, -f lets you watch a fresh crash. Caveat: docker logs only shows stdout/stderr. Empty logs mean either the app died before logging or logs go to a file inside the container.

Step 3: docker inspect --format — State, Restarts, Mounts, Health

If logs don't hand you the answer, ask Docker for structured facts:

docker inspect --format '{{ .State.Status }} {{ .State.ExitCode }}' web-1
docker inspect --format '{{ .RestartCount }}' web-1
docker inspect --format '{{ .State.OOMKilled }}' web-1
docker inspect --format '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }}{{ "\n" }}{{ end }}' web-1
docker inspect --format '{{ .State.Health.Status }}' web-1

.State.OOMKilled turns a guess into a fact. Saw 137 and suspect memory? This returns true or false. RestartCount confirms a real loop. Mounts check catches volume path mismatches.

Step 4: docker exec -it ... sh — Get Inside

If the container is running, step inside:

docker exec -it web-1 sh
# or bash
docker exec -it web-1 bash

Inside, check things logs can't show:

cat /etc/app/app.conf
env | sort
nslookup db-1
wget -qO- http://db-1:5432 || echo "cannot reach db-1"
df -h

If the container already exited, override the entrypoint:

docker run --rm -it --entrypoint sh myapp:1.4.2

Now you're in the filesystem without the failing startup command.

Step 5: docker stats — Resource Pressure

For slow or OOM-killed containers:

docker stats
docker stats --no-stream web-1

Watch MEM USAGE / LIMIT. If memory is pinned against the limit, you've found your 137. High steady CPU explains sluggishness. The slow leak looks like a container that "randomly" restarts every few hours.

Step 6: docker events — Who's Acting on This Container?

If the container dies but nothing inside explains why, widen the lens:

docker events --filter container=web-1
docker events --since 30m --filter container=web-1

This catches external actors — orchestrator killing, health check restarting, a script running docker kill.

Worked Example

web-1 is stuck restarting. docker ps -a shows Restarting (137). docker logs --tail 50 web-1 shows the app booting normally then stopping mid-request, no exception. docker inspect --format '{{ .State.OOMKilled }}' web-1 returns true. docker stats --no-stream web-1 confirms memory at the limit. Fix: increase memory limit or profile the leak. Six commands, no guessing.

Making It Repeatable

The order is the key: status, logs, inspect, exec, stats, events. Write it as a runbook for your team. When something breaks at 3 a.m., the person on call executes a routine, not improvises.

For reference versions of Docker errors, the author keeps guides at devopsaitoolkit.com/categories/docker and a broader toolkit at devopsaitoolkit.com/stacks/docker.

Wrapping up: start cheap, move up one layer at a time. Read the exit code, read the logs, confirm with inspect, then go inside. Fix the order in your head once, and every future incident gets quieter.