Restart Policies, Inspect, and Resource Limits
Measured on Docker Desktop 4.81.0 / Engine 29.6.1 (macOS, linux/arm64), 2026-09-30.
Restart policies
Section titled “Restart policies”By default a container that stops or crashes stays stopped. Restart policies tell Docker when to restart it automatically. Sources: Start containers automatically and the measurements below.
| Policy | Behaviour |
|---|---|
no | Never restart (default). |
always | Restart whenever it exits. If you stop it manually it stays stopped until the Docker daemon restarts (or you start it yourself). Starts on daemon boot. |
unless-stopped | Like always, but a container you stopped stays stopped even after the daemon restarts. |
on-failure[:N] | Restart only when the exit code is non-zero. Optional max retry count. Not restarted when the daemon restarts. |
# Recommended for long-running servicesdocker run -d --restart unless-stopped nginx:1.31-alpine
# Restart up to 5 times on failuredocker run -d --restart on-failure:5 myworker:latestAll four, produced
Section titled “All four, produced”Five containers, each running a command that exits immediately, inspected after 8 seconds with docker inspect -f 'policy={{.HostConfig.RestartPolicy.Name}}:{{.HostConfig.RestartPolicy.MaximumRetryCount}} status={{.State.Status}} exit={{.State.ExitCode}} restarts={{.RestartCount}}':
sh -c 'exit 1' --restart no policy=no:0 status=exited exit=1 restarts=0sh -c 'exit 1' --restart on-failure:3 policy=on-failure:3 status=exited exit=1 restarts=3sh -c 'exit 1' --restart always policy=always:0 status=restarting exit=1 restarts=7sh -c 'exit 1' --restart unless-stopped policy=unless-stopped:0 status=restarting exit=1 restarts=7sh -c 'exit 0' --restart on-failure:3 policy=on-failure:3 status=exited exit=0 restarts=0noran once and stayedexited.on-failure:3restarted exactly 3 times (RestartCount=3), then gave up:exitedwith exit 1. With exit code 0 it did not restart at all.alwaysandunless-stoppednever give up: after 8 s they were on their 7th restart, statusrestarting. The gaps are getting longer — Docker waits 100 ms before the first restart and doubles the wait each time, up to one minute, resetting it once the container has run for at least 10 seconds (moby restartmanager). A crash loop is therefore cheap but invisible;docker psshowsRestarting (1).
What docker stop does to a policy
Section titled “What docker stop does to a policy”docker run -d --restart always alpine:3.24 sleep 300 ; docker stop -t 1 ... -> status=exited exit=137 (still exited 3 s later)docker run -d --restart unless-stopped alpine:3.24 sleep 300 ; docker stop -t 1 ... -> status=exited exit=137 (still exited 3 s later)Neither restarts a container you stopped by hand while the daemon keeps running. The difference between them only appears when the daemon restarts: always brings the manually stopped container back, unless-stopped does not (per the docs table above; a daemon restart would disturb every container on this shared host, so it was not reproduced). Older text that says always restarts “even after a manual docker stop” is wrong for a running daemon.
Changing a policy on an existing container
Section titled “Changing a policy on an existing container”docker update --restart unless-stopped webdocker update --restart no webdocker update --restart no on the crash-looping always container ended the loop: status=exited restarts=7 and the count stopped rising.
Inspecting a container
Section titled “Inspecting a container”docker inspect returns the full JSON configuration of a container — network settings, mounts, environment variables, restart policy, and more:
docker inspect webserverTo pull out a single field use --format with Go template syntax:
# Get the container's IP address on each network it is attached todocker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' webserver
# Get the restart policydocker inspect --format '{{.HostConfig.RestartPolicy.Name}}' webserver
# The image reference, status and commanddocker inspect --format '{{.Config.Image}} | {{.State.Status}} | {{json .Config.Cmd}}' webserverReal output for those on a container running sleep 60: 172.17.0.3, no, and alpine:3.24 | running | ["sleep","60"]. {{json .X}} prints a value as JSON (a missing health check prints null).
--format is a Go template, so a wrong path is an error, not an empty string. Engine 29 removed the old top-level network fields:
$ docker inspect --format '{{.NetworkSettings.IPAddress}}' webtemplate parsing error: template: :1:18: executing "" at <.NetworkSettings.IPAddress>: map has no entry for key "IPAddress"Older tutorials use .NetworkSettings.IPAddress (and .Gateway); use the per-network form ({{range .NetworkSettings.Networks}}…{{end}} or {{.NetworkSettings.Networks.bridge.IPAddress}}). The IPs are the Desktop VM’s; from a Mac they are not routable.
Live resource monitoring with docker stats
Section titled “Live resource monitoring with docker stats”docker stats shows a live, updating table of CPU, memory, network I/O, and disk I/O for running containers:
# All running containersdocker stats
# Specific container, one snapshot (no live update)docker stats --no-stream webserverReal output with two containers each running a busy loop, one limited to half a CPU:
NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDSdocker-b1-cpu 51.91% 560KiB / 64MiB 0.85% 0B / 0B 0B / 0B 1docker-b1-cpu2 102.24% 560KiB / 64MiB 0.85% 0B / 0B 0B / 0B 1CPU % is relative to one core (100% = one core, a container can exceed 100% on several cores). The --cpus 0.5 container sits at about 50%; the unlimited single-threaded loop at 100%. MEM % is usage over the limit, PIDS the number of processes in the container’s cgroup. NET I/O and BLOCK I/O are cumulative counters, not rates.
Resource limits
Section titled “Resource limits”Containers can consume all available memory and CPU if left unconstrained. Set limits at run time:
docker run -d \ --name limited \ --memory 256m \ --cpus 0.5 \ nginx:1.31-alpine--memory 256m— hard memory cap (the container is OOM-killed if it exceeds this)--cpus 0.5— allow the container to use at most half a CPU core
Verify the limits are applied:
docker inspect --format 'Memory={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}}' limited# Memory=268435456 NanoCpus=500000000 (256 * 1024 * 1024 bytes; 0.5 CPU = 500000000 units of 1e-9 CPU)--cpus 0.5 is stored as NanoCpus=500000000 and shows up in the cgroup as cpu.max = 50000 100000 (50 ms of CPU per 100 ms period, see containers-vs-vms).
The out-of-memory kill
Section titled “The out-of-memory kill”A process that goes over --memory is killed by the kernel, not by Docker. With --memory 16m --memory-swap 16m and a Node process that allocates 100 MiB:
$ docker run --name oom --memory 16m --memory-swap 16m node:24-alpine node -e "Buffer.alloc(100*1024*1024,1)"$ echo $?137$ docker inspect -f 'OOMKilled={{.State.OOMKilled}} exit={{.State.ExitCode}} mem={{.HostConfig.Memory}}' oomOOMKilled=true exit=137 mem=16777216137 = 128 + 9 (SIGKILL) and OOMKilled=true tells you the kernel did it. Without --memory-swap equal to --memory, the container may also use swap of the same size before dying. Pair --memory with a restart policy and you have a crash loop with an OOM reason, visible only in docker inspect.
# 1. Run nginx with a restart policy
docker run -d --name webserver --restart unless-stopped nginx:1.31-alpine
# 2. Inspect the full config (JSON)
docker inspect webserver | head -60
# 3. Extract just the restart policy
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' webserver
# 4. Live stats (one snapshot)
docker stats --no-stream webserver
# 5. Run a memory-limited container
docker run -d --name limited --memory 128m --cpus 0.25 nginx:1.31-alpine
# 6. Confirm the memory limit
docker inspect --format '{{.HostConfig.Memory}}' limited
# 7. Clean up
docker stop webserver limited && docker rm webserver limitedReading AI-generated code
Section titled “Reading AI-generated code”An AI-written monitoring script that prints where each container lives:
🐛 Spot the bug
#!/bin/sh# AI: "print the IP of every running container"for c in $(docker ps -q); doname=$(docker inspect --format '{{.Name}}' "$c")ip=$(docker inspect --format '{{.NetworkSettings.IPAddress}}' "$c")echo "$name $ip"doneShow the answer
.NetworkSettings.IPAddress was removed from the top level of docker inspect in Engine 29, so the script dies with a template error on the first container (real text above: map has no entry for key "IPAddress"). Tutorials and older AI training data still use it. Range over the networks, which also handles containers on several networks or none:
#!/bin/shset -eufor c in $(docker ps -q); do docker inspect --format '{{.Name}} {{range $net, $cfg := .NetworkSettings.Networks}}{{$net}}={{$cfg.IPAddress}} {{end}}' "$c"doneOutput form: /web bridge=172.17.0.3. A container started with --network host prints host=invalid IP (it has no address of its own — it shares the engine host’s stack). And remember the addresses are inside the Docker Desktop VM.
Mental Model
Section titled “Mental Model”A restart policy is a tiny supervisor inside the daemon: it watches the exit of PID 1 and, if the policy says so and you did not stop the container yourself, starts it again after a growing delay. Limits are cgroup files, enforced by the kernel: exceeding memory means SIGKILL (137, OOMKilled=true), exceeding CPU just means being slowed down. docker inspect is the ground truth (HostConfig = what you asked for, State = what happened); docker stats is the live counter.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
--restart always | Auto-heals a crashing container with no one watching; comes back after a daemon restart even if you stopped it | Hides crash loops — a container that keeps dying looks “fine” because Docker keeps restarting it (7 restarts in 8 s here) |
--restart unless-stopped | Survives reboots and crashes, respects a deliberate stop | Same crash-loop blindness |
--restart on-failure:N | Caps the retry count (exactly 3 here), avoids an infinite loop, surfaces problems because it eventually stops | If N is too low, the container may stop for good before the real issue (e.g. a dependency not being ready yet) resolves itself |
--memory | A leak cannot take the host down | The app is killed abruptly (137) when it hits the limit |
--cpus | Fair sharing between containers | Throttled latency under load |
Common mistakes
Section titled “Common mistakes”- Setting
--restart alwaysand never checkingdocker logs, so a crash-restart loop runs unnoticed in the background. - Running
docker restartthe moment a container has a problem without first checking the exit code fromdocker inspect --format '{{.State.ExitCode}}'(andOOMKilled), and fixing the wrong thing as a result. - Never setting
--memory/--cpus, letting one container consume enough resources to take down the host or its neighbors. - Using
docker inspecttemplates copied from old posts (.NetworkSettings.IPAddress), which fail on Engine 29.
The same layering exists in Kubernetes: restartPolicy (Always/OnFailure/Never) decides what happens when a container exits, and probes add health-based restarts (Pod lifecycle). Docker applies the exit-based half on one machine.
Common Misconceptions
Section titled “Common Misconceptions”- “
alwaysrestarts a container even afterdocker stop.” Not while the daemon keeps running: it stayedexitedafter 3 s. It returns only after a daemon restart. - “
on-failure:3means three tries in total.” It means three restarts: the first run plus 3 restarts,RestartCount=3. - “
--cpus 0.5makes the container use 50% of every core.” It is a quota of half a CPU’s time in total (100 ms period, 50 ms allowed). - “OOM kills are logged by the app.” The app gets no chance: the kernel sends SIGKILL. Look at
OOMKilledindocker inspect.