Docker & ContainersVI Β· Container LifecycleObserving the lifecycle
Container states, exit codes, and the event stream
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
β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:
$ docker ps --filter status=bogus -qError response from daemon: invalid filter 'status=bogus': invalid value for state (bogus): must be one of created, running, paused, restarting, removing, exited, dead| State | Meaning | Usual next state |
|---|---|---|
created | The spec is stored and the writable layer exists. Nothing has run. | running |
running | PID 1 is alive. | exited, paused, restarting |
paused | The cgroup freezer is holding every task. | running |
restarting | PID 1 exited and the restart policy is waiting out a backoff. | running or exited |
removing | docker rm is in progress. | gone |
exited | PID 1 has terminated. The writable layer still exists. | running or gone |
dead | Removal 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.
$ 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 CreatedIllustrative 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:
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.
$ docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} err={{.State.Error}}' webstatus=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:
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"
fidocker 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:
$ 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 workerIllustrative 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:
docker events --since 1h --until 0m --filter type=container --format jsonThe 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.
$ docker inspect --format '{{.RestartCount}} {{.HostConfig.RestartPolicy.Name}} {{.HostConfig.RestartPolicy.MaximumRetryCount}}' worker57 on-failure 0Illustrative 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
Q1. A container exits with code 127. What does that tell you?
Q2. A container shows `Exited (137)` and `.State.OOMKilled` is `false`. Which causes remain plausible? Select all that apply.
Q3. A script that runs `docker ps | grep "Up"` to check container health will also pass for a container reporting `Up 2 minutes (unhealthy)`.
Q4. Which command tells you the exit code of a container three restarts ago?
Passing score: 75%. Answers are checked in this browser.