Docker Restart Policies and Resource Limits
What actually happens when a container dies, and what stops one container taking the host down with it. Includes a real OOM kill, and the cgroup files that prove a limit was applied.
Operations and Troubleshooting Guide 30 of 46 Intermediate
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- cgroupv2, systemd driver
- Architectureamd64
- TimeAbout 14 min
- Reviewed21 August 2026
Tested on the versions above. Limits are enforced by cgroup v2 here. On cgroup v1 hosts the file paths differ but the flags are the same.
| 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
- You can run containers in the background - guide 5 in this path.
- Reading container state with inspect - guide 7 in this path.
-
An unrestricted container can take the host with it
By default a container has no memory or CPU ceiling. It can allocate until the host runs out, at which point the kernel starts killing processes - and it will not necessarily kill the container that caused it. Limits are not an optimisation, they are the thing that keeps one bad deploy from being an outage.
bash Example session docker info --format "cgroup={{.CgroupVersion}} driver={{.CgroupDriver}}"cgroup=2 driver=systemdExpected resultcgroup v2 on any current Ubuntu.
Success conditionYou know which cgroup version you are on. It decides which files inside the container report the limits.
-
Set memory, CPU and process limits
--memorycaps RAM,--cpuscaps CPU time as a fraction of one core, and--pids-limitcaps process count - the last one is the cheap defence against a fork bomb. Setting--memory-swapequal to--memorydisables swap for the container, which is what you want if you would rather fail fast than thrash.bash Example session docker run -d --name cg-lim --memory 64m --memory-swap 64m --cpus 0.5 --pids-limit 50 nginx:alpine9b7fe22444d19896e6e276dc01c53b5cfebfeb631ec216207a3967b9b1a46c7edocker inspect cg-lim --format "mem={{.HostConfig.Memory}} nanocpus={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}"mem=268435456 nanocpus=500000000 pids=64Expected resultThe limits recorded in bytes and nanocpus rather than the units you typed.
Success condition
mem=67108864is 64 MiB. Docker stores the resolved value, so this is also how you check what a running container was actually given. -
Prove the limit reached the kernel
Do not take the daemon's word for it. Inside the container, cgroup v2 exposes the effective limits as plain files.
cpu.maxreads as quota and period - 50000 out of every 100000 microseconds is the half core you asked for.bash docker exec cg-lim cat /sys/fs/cgroup/memory.max67108864docker exec cg-lim cat /sys/fs/cgroup/cpu.max50000 100000docker exec cg-lim cat /sys/fs/cgroup/pids.max50Expected resultThe kernel reporting exactly the numbers you set.
Verify it worked
bash Example session docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}" cg-limNAME CPU % MEM USAGE / LIMIT MEM %cg-lim 0.00% 3.41MiB / 64MiB 5.33%Success condition
docker statsshows usage against the limit rather than against host RAM. Without a limit that column shows total host memory, which is the tell that no limit was applied. -
What exceeding the memory limit looks like
The container is killed, not slowed. The exit status is 137, which is 128 plus signal 9 - the same code you get from any SIGKILL, so the exit code alone does not tell you it was memory. The
OOMKilledflag does, and it is the only reliable way to distinguish an out-of-memory kill from someone runningdocker kill.bash # deliberately allocates past its own limit - contained by the limit, the host is unaffecteddocker run --name cg-oom --memory 64m --shm-size 256m alpine:3.22 sh -c "dd if=/dev/zero of=/dev/shm/fill bs=1M count=200"# killed - exit status 137docker inspect cg-oom --format "oomkilled={{.State.OOMKilled}} exit={{.State.ExitCode}} status={{.State.Status}}"oomkilled=true exit=137 status=exitedExpected result
oomkilled=truealongside exit 137.Success conditionYou can now tell an OOM kill from any other SIGKILL. Check this flag first whenever a container dies with 137.
-
Restart policies, and what they count
--restart on-failure:Nretries a container that exits non-zero, up to N times, then stops trying.alwaysretries forever and also starts the container when the daemon starts.unless-stoppedis the same but does not resurrect a container you stopped by hand - which is usually the one you want on a server.bash Example session docker run -d --name cg-flap --restart on-failure:3 alpine:3.22 sh -c "echo starting; sleep 1; exit 1"acc2ca5b7f8a2153eb51270f32001fa2a811723a6508cd85b12b1a477d5c8f16docker inspect cg-flap --format "restarts={{.RestartCount}} status={{.State.Status}} policy={{.HostConfig.RestartPolicy.Name}} max={{.HostConfig.RestartPolicy.MaximumRetryCount}}"restarts=3 status=exited policy=on-failure max=3Expected resultExactly three restarts, then Docker gives up and leaves it exited.
Verify it worked
bash Example session docker ps -a --filter name=cg-flap --format "table {{.Names}}\t{{.Status}}"NAMES STATUScg-flap Exited (1) 4 seconds agoSuccess condition
RestartCountmatches the cap. A container stuck atRestartinginstead has an unbounded policy - see guide 33 for how to break that loop. -
Change limits and policy without recreating
docker updatechanges resource limits and the restart policy of an existing container. It is the emergency lever: it can stop a restart loop, or raise a limit that is killing a service, without a redeploy. It cannot change ports, image or command - those genuinely need a new container.bash docker update --restart=no cg-loopcg-loopdocker rm -f cg-flap cg-lim cg-oom# clean up the containers from this guideExpected resultThe container name echoed back by update, then the removals.
Success condition
docker ps -ano longer lists them. Remember any limit you set withupdateis lost when the container is recreated - put it in the Compose file to make it stick.
Troubleshooting
A container keeps dying with exit code 137
Why: SIGKILL. Either the memory limit was exceeded, or something killed it. Only the OOMKilled flag distinguishes the two.
Fix:Check the flag. If it is true, raise the limit or fix the leak; measure real usage with
docker statsbefore picking a new number.bash docker inspect NAME --format "{{.State.OOMKilled}} {{.State.ExitCode}}"docker stats shows the limit as total host memory
Why: No memory limit was set on that container, so the cgroup ceiling is the machine.
Fix:Set
--memory, ormem_limit/deploy.resources.limitsin Compose. An unlimited container is the one that takes the host down.bash docker stats --no-stream --format "{{.Name}} {{.MemUsage}}"The restart policy did not survive a reboot
Why:
on-failuredoes not start containers when the daemon starts - onlyalwaysandunless-stoppeddo.Fix:Use
unless-stoppedfor services that should come back with the host, and make sure the Docker service itself is enabled.bash docker inspect NAME --format "{{.HostConfig.RestartPolicy.Name}}"systemctl is-enabled dockerCPU limit seems to have no effect
Why:
--cpuscaps total CPU time, not which cores are used, and a mostly idle process never reaches the cap. It also will not make a single-threaded process slower than one core.Fix:Verify the quota in the cgroup file rather than inferring it from behaviour. Use
--cpuset-cpuswhen you need specific cores.bash docker exec NAME cat /sys/fs/cgroup/cpu.max# quota period - "max 100000" means no limit