Docker Daemon and Container Events
The event stream is the timeline logs do not give you. Reproduce a health failure, an external kill and two memory kills, then use events to tell apart three containers that all exit 137.
Operations and Troubleshooting Guide 34 of 46 Advanced
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- Docker Compose5.4.0
- Architectureamd64
- TimeAbout 13 min
- Reviewed22 August 2026
Tested on the versions above. Every line below comes from one capture run on a real host: the containers were created together, one was killed deliberately, and every query covers that same window. Timestamps and container IDs are from that run.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
- Exit codes and container state - guide 33 in this path.
- Resource limits and the OOMKilled flag - guide 30 in this path.
-
Events are a timeline, logs are a transcript
Container logs tell you what an application said. Events tell you what the daemon did, in order, with timestamps. When something restarted at 03:14 and you need to know whether a health check failed first, or whether the kernel killed it, only events answer that. Bound the window with --since and --until; with neither, the command streams live and blocks.
bash docker events --since 30m --until "$END" --filter type=container --format "{{.Time}} {{.Actor.Attributes.name}} {{.Action}}" | head -41787232513 cg-ev-health create1787232513 cg-ev-health start1787232513 cg-ev-kill create1787232513 cg-ev-kill start# --since accepts 30m, 1h, or an absolute timestamp# with no --until the command streams live instead of returningExpected resultA timeline of container actions, oldest first.
Success conditionYou can query history rather than only watching live. Historical queries are what you need during an incident; live streaming is for while you reproduce something.
-
Reproduce three different failures
Reading events is only worth practising against events worth reading. These three cover what actually happens in production: a container failing its health check, one killed from outside, and two hitting a memory limit in different ways. Note the timestamps are recorded first so every query below covers exactly this window.
bash START=$(date -u +%Y-%m-%dT%H:%M:%S)docker run -d --name cg-ev-health --health-cmd "exit 1" --health-interval 2s --health-retries 2 nginx:alpinedocker run -d --name cg-ev-kill alpine:3.22 sleep 300docker run -d --name cg-ev-oom --memory 64m --memory-swap 64m alpine:3.22 sh -c 'tail /dev/zero'docker run -d --name cg-ev-shm --memory 64m --shm-size 256m alpine:3.22 sh -c 'head -c 200000000 /dev/zero > /dev/shm/fill'sleep 14; docker kill cg-ev-killcg-ev-killEND=$(date -u +%Y-%m-%dT%H:%M:%S)docker ps -a --filter name=cg-ev- --format "{{.Names}} {{.Status}}"cg-ev-shm Exited (137) 16 seconds agocg-ev-oom Exited (137) 17 seconds agocg-ev-kill Exited (137) 3 seconds agocg-ev-health Up 17 seconds (unhealthy)Expected resultThree containers exited and one running but unhealthy.
Success conditionThree of the four exited 137. That is the problem this guide solves - the same exit code for three different causes.
tail /dev/zeroreads endlessly into memory; the other writes past the memory limit into a tmpfs. -
Read the whole timeline first
Before filtering, look at the sequence. Every lifecycle transition is here in order, and two things stand out: the memory kills produce an explicit oom action before their die, and a health check is noisy - exec_create, exec_start and exec_die on every single interval.
bash docker events --since "$START" --until "$END" --filter type=container --format "{{.Time}} {{.Actor.Attributes.name}} {{.Action}}" | head -161787232513 cg-ev-health create1787232513 cg-ev-health start1787232513 cg-ev-kill create1787232513 cg-ev-kill start1787232513 cg-ev-oom create1787232513 cg-ev-oom start1787232513 cg-ev-oom oom1787232513 cg-ev-shm create1787232514 cg-ev-oom die1787232514 cg-ev-shm start1787232514 cg-ev-shm oom1787232514 cg-ev-shm die1787232515 cg-ev-health exec_create: /bin/sh -c exit 11787232515 cg-ev-health exec_start: /bin/sh -c exit 11787232515 cg-ev-health exec_die1787232517 cg-ev-health exec_create: /bin/sh -c exit 1Expected resultSixteen lines covering creation, the two oom events, the deaths and the health-check execs.
Success conditionYou can see cause before effect.
cg-ev-oom oomlands immediately beforecg-ev-oom die- that ordering is the evidence, not an inference. -
Every container that stopped, and why
This one query answers most "what happened overnight" questions. The exit code travels as an attribute of the die event, so you get the outcome without inspecting anything.
bash docker events --since "$START" --until "$END" --filter event=die --format "{{.Actor.Attributes.name}} died exitCode={{index .Actor.Attributes \"exitCode\"}}"cg-ev-oom died exitCode=137cg-ev-shm died exitCode=137cg-ev-kill died exitCode=137Expected resultThree containers, all exit 137.
Success conditionThis is the trap. Exit 137 means the process received SIGKILL. It says nothing about who sent it - the kernel's OOM killer and an operator with
docker killproduce the identical code. -
Tell an OOM apart from a kill
Two more filters separate them completely. The oom event fires only when the kernel's out-of-memory killer acts on the container's cgroup. The kill event fires when something asked the daemon to stop it. Each container's own stream shows exactly one of the two.
bash docker events --since "$START" --until "$END" --filter event=oom --format "oom: {{.Actor.Attributes.name}}"oom: cg-ev-oomoom: cg-ev-shm# cg-ev-kill is absent - it was not a memory problemExpected resultTwo of the three named, and the third missing.
Verify it worked
bash Example session docker events --since "$START" --until "$END" --filter container=cg-ev-oom --format "{{.Action}}"createstartoomdie docker events --since "$START" --until "$END" --filter container=cg-ev-kill --format "{{.Action}}"createstartkilldieSuccess conditionYou have attribution rather than a guess. Look at the individual streams and the difference is explicit.
-
The container's own state agrees
Events are the timeline; the container object carries the same conclusion as a flag. Use whichever is available - events after a container has been removed, inspect while it still exists.
bash docker inspect cg-ev-oom --format "oomkilled={{.State.OOMKilled}} exit={{.State.ExitCode}}"oomkilled=true exit=137docker inspect cg-ev-shm --format "oomkilled={{.State.OOMKilled}} exit={{.State.ExitCode}}"oomkilled=true exit=137docker inspect cg-ev-kill --format "oomkilled={{.State.OOMKilled}} exit={{.State.ExitCode}}"oomkilled=false exit=137# same exit code on all three, and OOMKilled is what separates themExpected resultTwo containers with oomkilled=true and one with false, all exit 137.
Success conditionBoth memory kills set the flag regardless of how the memory was consumed - one filled anonymous memory, the other a tmpfs, and the cgroup limit caught both. See guide 30 for setting those limits deliberately.
-
Catch degradation that never restarts anything
A service that goes unhealthy and recovers repeatedly never restarts, so nothing watching container state will notice. health_status is the only place it surfaces, and it is worth alerting on.
bash docker events --since "$START" --until "$END" --filter event=health_status --format "{{.Actor.Attributes.name}} -> {{.Action}}" | head -4cg-ev-health -> health_status: unhealthy# flapping appears here as repeated healthy / unhealthy transitionsExpected resultThe transition to unhealthy as its own event.
Success conditionYou have a signal for a container that is failing without dying. Pair it with guide 26.
-
One query for an incident
During an incident you rarely want one event type. Filters of the same kind are OR-ed, so this single command gives deaths, memory kills and health transitions on one timeline - which is usually the whole picture.
bash docker events --since "$START" --until "$END" --filter event=die --filter event=oom --filter event=health_status --format "{{.Action}} {{.Actor.Attributes.name}}"oom cg-ev-oomdie cg-ev-oomoom cg-ev-shmdie cg-ev-shmhealth_status: unhealthy cg-ev-healthdie cg-ev-killExpected resultAll three failure classes interleaved in the order they happened.
Success conditionThe oom lines sit immediately before their die lines, and the health transition sits on its own. This is the query to reach for first.
-
Events cover more than containers
Networks, volumes, images, plugins and the daemon itself all emit events. Network attach and detach are the ones you will want when a container cannot reach something and you need to know whether it ever joined the network at all.
bash docker events --since "$START" --until "$END" --filter type=network --format "{{.Type}} {{.Action}} {{.Actor.Attributes.name}}" | head -4network connect bridgenetwork connect bridgenetwork connect bridgenetwork disconnect bridge# also available: --filter type=volume, type=image, type=daemonExpected resultAttach and detach events for the bridge network.
Success conditionYou know the stream is broader than containers. With no type filter it shows everything, which is unreadable live but useful scoped to a window.
-
Clean up
Remove the four containers created to generate these events.
bash docker rm -f cg-ev-health cg-ev-oom cg-ev-kill cg-ev-shmcg-ev-healthcg-ev-oomcg-ev-killcg-ev-shmdocker ps -aq | wc -l0Expected resultThe four names echoed back and no containers left.
Success conditionThe host is back where it started.
Troubleshooting
docker events hangs and returns nothing
Why: With no --until it streams live and waits for something to happen.
Fix:Give it a bounded window to query history, or trigger an action in another terminal while it streams.
bash docker events --since 1h --until "$(date -u +%Y-%m-%dT%H:%M:%S)"Events from before a certain point are missing
Why: The daemon holds a bounded in-memory buffer. Events do not survive a daemon restart.
Fix:Ship them somewhere if you need them retained. Treat the built-in stream as a short window, not an audit trail.
bash docker events --format "{{json .}}" >> /var/log/docker-events.jsonl &Too many events to read on a busy host
Why: A single health-checked container emits three exec events on every interval.
Fix:Filter by event type and bound the window. Filters of the same kind are OR-ed together.
bash docker events --since 1h --filter event=die --filter event=oom --filter event=health_statusA container restarted and no event says why
Why: The restart policy acted after the process exited. The reason is in the die event that precedes the start, not in the restart.
Fix:Read the die event immediately before the start, then the logs of the previous instance.
bash docker events --since 1h --filter container=NAME --format "{{.Action}} {{index .Actor.Attributes \"exitCode\"}}"