Docker & ContainersXXX Β· Host MaintenanceVerification
Post-maintenance verification β proving the host came back
What you'll learn
- Verify a Docker host after maintenance in seven ordered layers
- Distinguish a container that is running from one that is working
- Detect firewall and network state that did not survive the reboot
- Close a maintenance window with evidence a reviewer can check
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 host rebooted, docker ps shows the containers, and the temptation is to
close the window. That evidence is weaker than it looks: a container in the
Up state has had its entrypoint executed and nothing more. It says nothing
about whether the application inside is serving, whether the port is reachable,
or whether the firewall rules that put traffic there came back.
This lesson is the verification that ends every procedure in this part. Seven layers, from the machine upward, in the order that finds problems fastest.
Layer 1: the host itself
# The kernel you intended to boot
uname -r
# Did every unit start?
systemctl is-system-running
systemctl --failed --no-pager
# Uptime confirms this is the boot you think it is
uptime -p
who -b
# Filesystems all mounted, with room
findmnt -t ext4,xfs -o TARGET,SOURCE,SIZE,USED,AVAIL,USE%
df -i /var/lib/dockersystemctl is-system-running returning degraded means at least one unit
failed, and systemctl --failed names it. This is the single fastest way to
find a mount that did not come up or a service that failed before Docker
started β both of which explain a container that will not start, three layers
further up.
Layer 2: the daemons
systemctl is-active containerd docker
docker version --format 'server={{.Server.Version}}'
docker info --format 'containers={{.Containers}} running={{.ContainersRunning}} images={{.Images}}'
# Warnings the daemon prints about the environment it found
docker info 2>&1 | grep -iE '^WARNING|no swap|no cpu' || echo 'no daemon warnings'
# Anything logged during startup
journalctl -u docker --since "$(uptime -s)" --no-pager -p warningThe daemon warnings matter after a kernel change. WARNING: No swap limit support appearing for the first time means the new kernel booted without
swapaccount=1, and every memory limit on the host now behaves differently
from how it did yesterday.
Layer 3: the container set
Not βare containers runningβ but βare the right containers runningβ.
docker ps --format '{{.Names}}' | sort > /tmp/running.after
diff /tmp/running.before /tmp/running.after && echo 'PASS: identical container set' || echo 'FAIL: see diff'
# Anything that exists but is not running
docker ps -a --filter status=exited --filter status=created --format '{{.Names}} {{.Status}}'$ diff /tmp/running.before /tmp/running.after5d4
< workerIllustrative output
Without the baseline this is invisible. Eleven containers running looks exactly like twelve containers running unless you are counting, and nobody counts at 03:40.
Layer 4: container health
docker ps --format 'table {{.Names}} {{.Status}}'
# Anything explicitly unhealthy
docker ps --filter health=unhealthy --format '{{.Names}}'
# Anything still starting - these need another look shortly
docker ps --filter health=starting --format '{{.Names}}'
# Why a specific container is unhealthy
docker inspect --format '{{json .State.Health}}' webLayer 5: networking on the host
The layer that fails silently after a reboot, because Docker rebuilds most of it and something else does not.
# Networks exist and containers are attached
docker network ls
docker network inspect bridge --format '{{len .Containers}} container(s) attached'
# Ports are actually published and listening on the host
docker ps --format 'table {{.Names}} {{.Ports}}'
sudo ss -tlnp | grep -E 'docker-proxy|dockerd' || echo 'no docker-proxy listeners'
# Docker's own NAT rules were re-applied
sudo iptables -t nat -L DOCKER -n --line-numbers | head -20sudo iptables -L DOCKER-USER -n --line-numbers
sudo systemctl is-active nftables netfilter-persistent 2>/dev/null || trueLayer 6: the application, from the host
Bypass the load balancer. You are asking whether the container works, not whether the routing works β those are two failures with two different fixes, and separating them saves a lot of time.
curl -fsS -o /dev/null -w 'local %{http_code} in %{time_total}s\n' http://127.0.0.1:8080/healthz
# And from inside the network, by service name, to test embedded DNS
docker run --rm --network app_default curlimages/curl:8.10.1 -fsS -o /dev/null -w 'internal %{http_code}\n' http://web:8080/healthzThe second command is the one that catches a broken embedded DNS resolver or a container attached to the wrong network β both of which leave the application reachable from the host and invisible to its own peers.
Layer 7: the application, as a user
curl -fsS -o /dev/null -w 'end-to-end %{http_code} dns=%{time_namelookup}s tls=%{time_appconnect}s total=%{time_total}s\n' https://app.example.com/healthz
# And confirm the certificate did not expire while you were not looking
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -datesOnly this layer proves the thing the maintenance window was supposed to preserve. Everything above it is diagnosis for when this one fails.
The whole thing as a script
#!/usr/bin/env bash
set -uo pipefail
fail=0
chk() { if [ "$1" -eq 0 ]; then echo "PASS $2"; else echo "FAIL $2"; fail=1; fi; }
test -z "$(systemctl --failed --no-pager --plain --no-legend)"
chk $? "L1 no failed systemd units"
systemctl is-active --quiet docker containerd
chk $? "L2 docker and containerd active"
docker ps --format '{{.Names}}' | sort > /tmp/running.after
diff -q /tmp/running.before /tmp/running.after > /dev/null
chk $? "L3 container set matches baseline"
test -z "$(docker ps --filter health=unhealthy -q)"
chk $? "L4 no unhealthy containers"
sudo iptables -t nat -L DOCKER -n | grep -q DNAT
chk $? "L5 docker NAT rules present"
curl -fsS -o /dev/null --max-time 5 http://127.0.0.1:8080/healthz
chk $? "L6 application responds locally"
curl -fsS -o /dev/null --max-time 10 https://app.example.com/healthz
chk $? "L7 application responds end to end"
exit "$fail"Seven lines of output, each of which is either PASS or FAIL, and an exit code you can put in a change ticket. That is what closing a maintenance window should look like.
Knowledge check
Knowledge check Β· 4 questions
Q1. After a reboot, `docker ps --filter health=unhealthy` returns nothing. What does that prove?
Q2. Which firewall state is Docker responsible for restoring after a reboot?
Q3. Why verify the application from the host before verifying it end to end? Select all that apply.
Q4. A verification that passes all seven layers means the maintenance window can be closed with confidence that nothing was missed.
Passing score: 75%. Answers are checked in this browser.