Skip to main content
RunBook Academy

Docker & ContainersXXXIII · Incident ResponseTriage

Triage — the first 15 minutes of an incident

Intermediate⏱ ~28 mindocker

What you'll learn

  • Triage a Docker incident in the first 15 minutes
  • Distinguish symptoms from causes
  • Escalate appropriately
  • Capture volatile evidence before any action destroys it
  • Choose a mitigation whose blast radius you have measured

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-12

Not yet marked complete on this device.

The first 15 minutes of an incident determine whether you mitigate or escalate. The wrong move is to dive into root cause analysis before you stabilise.

There is a precondition that “stabilise first” usually leaves out, and in Docker it matters more than anywhere else. The fastest stabilisation available to you is a restart. A restart destroys the exit code, the error string, the OOM flag, the log buffer and the cgroup counters of the thing that failed. So the correct instruction is not “stabilise first” — it is capture, then stabilise, and the capture takes ninety seconds.

The clock

  1. Minute 0-1: acknowledge. Tell the channel you are on it. Start a timestamped log of what you do, in the channel, as you do it. If you cannot take it, page someone who can and say so immediately.
  2. Minute 1-3: capture. Run the snapshot below. Do not skip it because it feels like delay — everything after this point destroys evidence, and you get one chance.
  3. Minute 3-6: assess impact. What is broken, who is affected, is it getting worse, and is there a work-around? This is what determines severity and who else needs waking.
  4. Minute 6-11: stabilise. Mitigate user impact with the smallest action that could work. Roll back the deploy, restart the container, fail over, shed the bad traffic. Do not investigate the root cause yet.
  5. Minute 11-15: communicate and decide. Update the channel with state and next step. If you have not stabilised, escalate now rather than at minute 25.
  6. After 15 minutes: investigate. The system is stable and you have the evidence you captured at minute 1.

The minute-1 capture is the addition that turns this from advice into a procedure. Everything else in the list is standard incident practice; the capture is what makes it work on a Docker host specifically, because Docker’s state is unusually volatile and unusually easy to destroy by accident.

The ninety-second capture

Read-only / Safecapture before you touch anything
#!/usr/bin/env bash
# Read-only incident snapshot. Safe to run at any time.
set -uo pipefail
OUT=/var/tmp/incident-$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p "$OUT"
exec 2>>"$OUT"/capture-errors.txt

# --- Host, 5 seconds ---
date -u                                   > "$OUT"/00-when.txt
uptime; free -m; nproc                    > "$OUT"/01-host.txt
df -h; df -i                              > "$OUT"/02-disk.txt
cat /proc/pressure/cpu /proc/pressure/io /proc/pressure/memory \
                                        > "$OUT"/03-pressure.txt

# --- Docker, 20 seconds ---
docker ps -a --no-trunc                   > "$OUT"/10-ps.txt
docker stats --no-stream                  > "$OUT"/11-stats.txt
docker system df -v                       > "$OUT"/12-df.txt
docker info                               > "$OUT"/13-info.txt
docker events --since 2h --until now      > "$OUT"/14-events.txt

# --- Per container, the bulk of the time ---
for c in $(docker ps -aq); do
docker inspect "$c"                     > "$OUT"/inspect-"$c".json
docker logs --timestamps --tail 1000 "$c" > "$OUT"/logs-"$c".txt 2>&1
done

# --- Kernel and daemon, 10 seconds ---
dmesg -T | tail -500                      > "$OUT"/20-dmesg.txt
journalctl -u docker.service --since '2 hours ago' --no-pager \
                                        > "$OUT"/21-dockerd.txt
journalctl -u containerd.service --since '2 hours ago' --no-pager \
                                        > "$OUT"/22-containerd.txt

# --- Network, 5 seconds ---
ss -tanp                                  > "$OUT"/30-sockets.txt
iptables-save                             > "$OUT"/31-iptables.txt
docker network ls                         > "$OUT"/32-networks.txt

echo "captured to $OUT"
du -sh "$OUT"

Three details in that script are the difference between it working and not.

--until now on the events capture. Without it, docker events follows the stream forever and the script hangs at the worst moment of the worst day.

--no-stream on docker stats. Same reason.

set -uo pipefail without -e. This script must not abort partway through because one container’s logs are unreadable. A partial capture is worth having; no capture is not.

Put this script on every host, in /usr/local/bin, before you need it. Writing it during an incident is how you end up not running it.

What “stabilise” looks like, with blast radius

For a Docker incident, ordered by how much they disturb:

ActionBlast radiusReversible
docker restart <one container>One service, seconds of downtimeYes
Scale out: start another instanceNone if the LB is healthyYes
Shed load at the proxyThe shed traffic onlyYes
Roll back to the previous image tagOne service, needs the image to be pullableYes, forward
docker compose down && up with old tagThe whole stack, ordered stopYes
systemctl restart dockerEvery container on the host unless live-restore is onNo
Reboot the hostEverything, plus all volatile evidenceNo
Service impact possiblecheck the blast radius before the big hammer
# Would a daemon restart keep containers running?
docker info --format 'live-restore={{.LiveRestoreEnabled}}'

# How many containers would it take down if not?
docker ps --format '{{.Names}}' | wc -l
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

# Is this host the only one serving? Check before you drain it.
# (substitute your own load balancer query)
curl -sS -m 3 http://192.0.2.5:8404/stats\;csv 2>/dev/null | cut -d, -f1,2,18

The first line is the one that gets skipped. systemctl restart docker on a host with live-restore disabled and eleven containers running is an eleven-service outage, entered into voluntarily, usually to fix one of them.

Docker-specific triage shortcuts

Three questions worth asking early, because each has a one-command answer that eliminates a large branch.

Read-only / Safethe three fast eliminations
# 1. Did anything actually change on the Docker layer in the last hour?
docker events --since 1h --until now \
--format '{{.Time}} {{.Action}} {{.Actor.Attributes.name}}' | tail -40

# 2. Is anything unhealthy or restarting right now?
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}' \
| grep -Ei 'restarting|unhealthy|exited' || echo 'all containers stable'

# 3. Is the host itself the problem, rather than any container?
df -h --output=pcent,target | sort -rn | head -5
cat /proc/pressure/io /proc/pressure/memory
uptime

Question 1 is powerful in the negative. An empty event stream for the incident window means no container started, stopped, died, or was OOM-killed, which eliminates the entire “something crashed” family and points at the application or a dependency.

Question 3 catches the case where five services degraded at once, which is almost never five simultaneous application bugs and almost always a shared resource: disk, memory, or a saturated device.

Escalate on a rule, not on a feeling

When to escalate:

  • You cannot stabilise within 15 minutes.
  • The impact is growing — more users, more services, or a second host.
  • The cause is unknown and the symptoms are unfamiliar. Unknown cause with familiar symptoms is normal; unfamiliar symptoms mean your model is wrong.
  • The mitigation you are considering has a blast radius larger than the incident. Rebooting a host to fix one container is a decision that wants a second person on it.
  • More than one team’s system is involved.

How to escalate, in a form the next person can use:

Read-only / Safehandoff template
cat <<'EOF'
INCIDENT HANDOFF
Started (UTC):     2026-08-12T02:14Z
Symptom:           checkout API returning 502 for ~40% of requests
Impact:            all customers, checkout only; browse unaffected
Trend:             stable since 02:31, not worsening

Evidence captured: /var/tmp/incident-20260812T021600Z on app-01

Tried:
  02:18  captured snapshot
  02:22  restarted checkout container - 502s resumed after ~90s
  02:29  shed 50% traffic at HAProxy - error rate fell, latency normal
Ruled out:
  - not OOM: State.OOMKilled false, memory.events oom_kill 0
  - not disk: df 41% bytes, 22% inodes
  - not a deploy: no image change in docker events for 6h
Current hypothesis:
  connection pool exhaustion against the payments dependency

Next planned step:  raise pool size and observe; NOT yet done
I am:               staying on the call until you take over
EOF

The “ruled out” section is the part that saves the most time and the part people omit. Handing over a list of what is not wrong, with the evidence, stops the next responder repeating your first twenty minutes.

Symptoms vs causes

A symptom is “the application returns 500”. A cause is “the database connection pool is exhausted”.

You stabilise at the symptom level. You fix at the cause level. The two are different skills, and conflating them is what produces both of the classic failures: investigating while users are down, and declaring a restart to be a fix.

Verification: did the mitigation work?

A mitigation is verified by the user-facing signal, not by the container being up.

Read-only / Safeconfirm stabilisation
#!/usr/bin/env bash
set -euo pipefail
CONTAINER=checkout
URL=http://127.0.0.1:8080/healthz
SAMPLES=20

# Container is up and past the restart threshold
state=$(docker inspect "$CONTAINER" --format '{{.State.Status}}')
restarts=$(docker inspect "$CONTAINER" --format '{{.RestartCount}}')
[ "$state" = 'running' ] || { echo "FAIL: state=$state" >&2; exit 1; }

# The symptom itself is gone: sample the endpoint, count non-200s
fail=0
for _ in $(seq "$SAMPLES"); do
code=$(curl -sS -m 3 -o /dev/null -w '%{http_code}' "$URL" || echo 000)
[ "$code" = '200' ] || fail=$(( fail + 1 ))
sleep 1
done

echo "state=$state restarts=$restarts failures=$fail of $SAMPLES"
[ "$fail" -eq 0 ] || { echo 'FAIL: symptom still present' >&2; exit 1; }
echo 'STABLE'

Sampling over time rather than once is what distinguishes “it worked” from “the first request after a restart happened to succeed”. A container that fails one request in five is not stabilised, and a single curl has an 80% chance of telling you it is.

Knowledge check

Knowledge check · 7 questions

  1. Q1. In the first 15 minutes of an incident, the priority is:

  2. Q2. Why does a read-only capture come before mitigation on a Docker host specifically?

  3. Q3. Before running `systemctl restart docker` as a mitigation, which single check matters most?

  4. Q4. Which pieces of state are lost when you run `docker restart` on a failed container? Select all that apply.

  5. Q5. Which conditions justify escalating rather than continuing alone? Select all that apply.

  6. Q6. You should never change configuration during the first 15 minutes.

  7. Q7. Verifying a mitigation requires sampling the symptom over a window rather than probing once.

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