Skip to main content
RunBook Academy

Docker & ContainersVI Β· Container LifecycleObserving the lifecycle

Container states, exit codes, and the event stream

Intermediate⏱ ~24 mindocker

What you'll learn

  • Name the seven container states and what moves a container between them
  • Read an exit code and narrow the cause before opening the logs
  • Use `docker events` and `docker wait` to observe transitions instead of polling
  • Recognise a restart loop from its backoff pattern

Prerequisites

Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11

Not yet marked complete on this device.

β€œThe container is down” is not a diagnosis. A container that never started, a container that started and exited 0, a container the kernel killed, and a container stuck in a restart loop all look identical in a dashboard that only counts running containers β€” and each one needs a different next command.

The state and the exit code together narrow the problem before you read a single line of application log.

The seven states

The daemon will tell you the list itself if you ask for a state it does not recognise:

Read-only / Safestates
$ docker ps --filter status=bogus -q
Error response from daemon: invalid filter 'status=bogus': invalid value for state (bogus): must be one of created, running, paused, restarting, removing, exited, dead
StateMeaningUsual next state
createdThe spec is stored and the writable layer exists. Nothing has run.running
runningPID 1 is alive.exited, paused, restarting
pausedThe cgroup freezer is holding every task.running
restartingPID 1 exited and the restart policy is waiting out a backoff.running or exited
removingdocker rm is in progress.gone
exitedPID 1 has terminated. The writable layer still exists.running or gone
deadRemoval was attempted and failed part-way.needs intervention

dead is the one people rarely see and always misread. It means the daemon tried to tear the container down and could not β€” usually because a mount inside it was busy, or the storage driver returned an error. The container holds a partially released writable layer and cannot be started. The fix is to find the stuck mount (mount | grep <container-id>, lsof on the merged directory) and clear it, then docker rm -f.

STATE, STATUS, and the parsing mistake

docker ps shows two different things and only one of them is safe to parse.

Read-only / Safeps
$ docker ps -a --format 'table {{.Names}}\t{{.State}}\t{{.Status}}'
NAMES       STATE       STATUS
web         running     Up 4 days (healthy)
worker      running     Up 2 minutes (health: starting)
importer    exited      Exited (0) 3 hours ago
cache       restarting  Restarting (1) 12 seconds ago
staged      created     Created

Illustrative output

.State is one of the seven keywords. .Status is free text whose shape depends on health checks, uptime units, and exit codes. Scripts that test docker ps | grep Up return true for Up 2 minutes (unhealthy), which is exactly the case they were written to catch.

Use filters, which are evaluated by the daemon:

Read-only / Safefilters
docker ps --filter status=running --filter health=unhealthy --format '{{.Names}}'
docker ps -a --filter status=exited --filter exited=137 --format '{{.Names}}'
docker ps -a --filter status=created --format '{{.Names}}'

Reading an exit code

The exit code is stored in .State.ExitCode and is printed by docker ps in the Exited (N) status string. There are three bands.

Read-only / Safepost-mortem
$ docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} err={{.State.Error}}' web
status=exited exit=137 oom=true err=

Illustrative output

oom=true with exit 137 is the kernel OOM killer, and the fix is a memory limit or a memory leak. oom=false with exit 137 after a docker stop is your stop timeout being too short. Same exit code, opposite remedies.

.State.Error carries the runtime’s message when the container could not be started at all β€” the 126 and 127 cases usually leave something here.

Watching transitions instead of polling

Two commands replace a sleep 1; docker ps loop.

docker wait blocks until the container stops and prints its exit code. It is the correct way to run a one-shot container from a script:

Read-only / Safewait
docker start importer
rc=$(docker wait importer)
if [ "$rc" -ne 0 ]; then
  echo "import failed with status $rc" >&2
  docker logs --tail 50 importer >&2
  exit "$rc"
fi

docker events is a live stream of every lifecycle transition on the host. Filtered, it is the fastest way to see what is actually happening during a deploy or an incident:

Read-only / Safeevents
$ docker events --filter type=container --format '{{.Time}} {{.Action}} {{.Actor.Attributes.name}}'
1786462052 start web
1786462088 health_status: healthy web
1786462311 die worker
1786462311 start worker
1786462352 die worker
1786462354 start worker

Illustrative output

The event stream is also a post-mortem tool. --since and --until accept timestamps or relative durations, so you can replay the window around an incident after the fact:

Read-only / Safereplay
docker events --since 1h --until 0m --filter type=container --format json

The die action carries the exit code in Actor.Attributes.exitCode, which means you can answer β€œwhat exit code did it have three restarts ago” β€” a question docker inspect cannot answer, because it only ever holds the most recent exit.

Reading a restart loop

A container with a restart policy that keeps failing produces a distinctive pattern. The daemon backs off between attempts β€” roughly doubling from a tenth of a second up to a cap of one minute β€” so the events start dense and thin out.

Read-only / Saferestart count
$ docker inspect --format '{{.RestartCount}} {{.HostConfig.RestartPolicy.Name}} {{.HostConfig.RestartPolicy.MaximumRetryCount}}' worker
57 on-failure 0

Illustrative output

A MaximumRetryCount of 0 with on-failure means unlimited retries, which is how a container reaches 57 restarts without anybody being paged. Set a bound β€” --restart on-failure:5 β€” so that a genuinely broken container ends up exited and visible rather than restarting forever and invisible to a check that only counts non-running containers.

Knowledge check

Knowledge check Β· 4 questions

  1. Q1. A container exits with code 127. What does that tell you?

  2. Q2. A container shows `Exited (137)` and `.State.OOMKilled` is `false`. Which causes remain plausible? Select all that apply.

  3. Q3. A script that runs `docker ps | grep "Up"` to check container health will also pass for a container reporting `Up 2 minutes (unhealthy)`.

  4. Q4. Which command tells you the exit code of a container three restarts ago?

Passing score: 75%. Answers are checked in this browser.