Skip to content

Restart Policies, Inspect, and Resource Limits

Measured on Docker Desktop 4.81.0 / Engine 29.6.1 (macOS, linux/arm64), 2026-09-30.

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.

PolicyBehaviour
noNever restart (default).
alwaysRestart 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-stoppedLike 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.
Terminal window
# Recommended for long-running services
docker run -d --restart unless-stopped nginx:1.31-alpine
# Restart up to 5 times on failure
docker run -d --restart on-failure:5 myworker:latest

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=0
sh -c 'exit 1' --restart on-failure:3 policy=on-failure:3 status=exited exit=1 restarts=3
sh -c 'exit 1' --restart always policy=always:0 status=restarting exit=1 restarts=7
sh -c 'exit 1' --restart unless-stopped policy=unless-stopped:0 status=restarting exit=1 restarts=7
sh -c 'exit 0' --restart on-failure:3 policy=on-failure:3 status=exited exit=0 restarts=0
  • no ran once and stayed exited.
  • on-failure:3 restarted exactly 3 times (RestartCount=3), then gave up: exited with exit 1. With exit code 0 it did not restart at all.
  • always and unless-stopped never give up: after 8 s they were on their 7th restart, status restarting. 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 ps shows Restarting (1).
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”
Terminal window
docker update --restart unless-stopped web
docker update --restart no web

docker update --restart no on the crash-looping always container ended the loop: status=exited restarts=7 and the count stopped rising.

docker inspect returns the full JSON configuration of a container — network settings, mounts, environment variables, restart policy, and more:

Terminal window
docker inspect webserver

To pull out a single field use --format with Go template syntax:

Terminal window
# Get the container's IP address on each network it is attached to
docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' webserver
# Get the restart policy
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' webserver
# The image reference, status and command
docker inspect --format '{{.Config.Image}} | {{.State.Status}} | {{json .Config.Cmd}}' webserver

Real 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}}' web
template 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:

Terminal window
# All running containers
docker stats
# Specific container, one snapshot (no live update)
docker stats --no-stream webserver

Real 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 PIDS
docker-b1-cpu 51.91% 560KiB / 64MiB 0.85% 0B / 0B 0B / 0B 1
docker-b1-cpu2 102.24% 560KiB / 64MiB 0.85% 0B / 0B 0B / 0B 1

CPU % 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.

Containers can consume all available memory and CPU if left unconstrained. Set limits at run time:

Terminal window
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:

Terminal window
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).

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}}' oom
OOMKilled=true exit=137 mem=16777216

137 = 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 limited

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); do
name=$(docker inspect --format '{{.Name}}' "$c")
ip=$(docker inspect --format '{{.NetworkSettings.IPAddress}}' "$c")
echo "$name $ip"
done
Show 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:

scripts/container-ips.sh
#!/bin/sh
set -eu
for c in $(docker ps -q); do
docker inspect --format '{{.Name}} {{range $net, $cfg := .NetworkSettings.Networks}}{{$net}}={{$cfg.IPAddress}} {{end}}' "$c"
done

Output 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.

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.

OptionBenefitCost
--restart alwaysAuto-heals a crashing container with no one watching; comes back after a daemon restart even if you stopped itHides crash loops — a container that keeps dying looks “fine” because Docker keeps restarting it (7 restarts in 8 s here)
--restart unless-stoppedSurvives reboots and crashes, respects a deliberate stopSame crash-loop blindness
--restart on-failure:NCaps the retry count (exactly 3 here), avoids an infinite loop, surfaces problems because it eventually stopsIf N is too low, the container may stop for good before the real issue (e.g. a dependency not being ready yet) resolves itself
--memoryA leak cannot take the host downThe app is killed abruptly (137) when it hits the limit
--cpusFair sharing between containersThrottled latency under load
  • Setting --restart always and never checking docker logs, so a crash-restart loop runs unnoticed in the background.
  • Running docker restart the moment a container has a problem without first checking the exit code from docker inspect --format '{{.State.ExitCode}}' (and OOMKilled), 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 inspect templates 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.

  • “always restarts a container even after docker stop.” Not while the daemon keeps running: it stayed exited after 3 s. It returns only after a daemon restart.
  • “on-failure:3 means three tries in total.” It means three restarts: the first run plus 3 restarts, RestartCount=3.
  • “--cpus 0.5 makes 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 OOMKilled in docker inspect.
Which restart policy restarts a container only when it exits with a non-zero code?
What is the flag to set a hard memory limit of 256 MB?
What does docker stats --no-stream do?
A container with --restart always was stopped with docker stop and the daemon keeps running. What state is it in a few seconds later?
A container exits with code 137 and docker inspect shows OOMKilled=true. What happened?
docker inspect --format '{{.NetworkSettings.IPAddress}}' web fails on Engine 29. What works?