`unless-stopped` containers are not restarted after dockerd exits uncleanly: restore() marks the containers it stops as manually stopped
Maintainers usually reply within 1 day
@akasakariko is already working on this.
Since Oct 30, 2026.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 68/100
Research direction
Start by reading the restore path in daemon/daemon.go alongside shutdownContainer() and the HasBeenManuallyStopped assignment in daemon/kill.go; then check how daemon/internal/restartmanager/restartmanager.go uses that flag. Done means a container stopped during restore after an unclean daemon exit is not treated as manually stopped, and an unless-stopped container restarts as expected; verify that clean-shutdown behavior remains unchanged.
Written by the indexing model from the issue text.
Description
Description
When dockerd exits without a clean shutdown (crash, OOM kill, or SIGKILL from systemd after its stop timeout) with live-restore disabled, the container processes keep running under their shims. On the next start, restore() finds them alive and stops them. That stop goes through killWithSignal, which sets HasBeenManuallyStopped = true because the daemon is not shutting down. The restart manager then refuses to restart unless-stopped containers. They stay Exited (137) until someone starts them by hand. always containers come back.
A clean daemon restart is not affected. Shutdown() sets daemon.shutdown = true before stopping containers, so the flag is not set, and unless-stopped containers are restarted on the next start.
As a result, unless-stopped behaves like no in exactly the situation where automatic recovery matters most, even though nobody stopped these containers.
Code path (docker-v29.8.2):
daemon/daemon.go#L463-L470: with live-restore off,restore()callsshutdownContainer()for containers that containerd reports alive.daemon/daemon.go#L1482-L1494:shutdownContainer()callscontainerStop().daemon/kill.go#L95-L96:HasBeenManuallyStopped = truewhenever!daemon.IsShuttingDown(), which is the case during startup.daemon/internal/restartmanager/restartmanager.go#L82-L84:alwaysignores the flag;unless-stoppedrequires it to be false.daemon/daemon.go#L563:restore()only restarts containers for whichShouldRestart()is true.- For contrast,
daemon/daemon.go#L1534:Shutdown()setsdaemon.shutdown = truefirst.
Reproduce
containerd runs as its own process, as with the usual containerd.service + docker.service setup:
docker run -d --name us --restart unless-stopped busybox sleep 86400
docker run -d --name al --restart always busybox sleep 86400
kill -9 $(pidof dockerd) # container processes keep running
dockerd ... # start the daemon again (systemd does this with Restart=always)
docker ps -a # "us" is Exited (137), "al" is running
Results on Docker 29.8.2 / containerd v2.3.6 (docker:29.8.2-dind, containerd started separately; full script and output below):
| Scenario | unless-stopped |
always |
|---|---|---|
| A. dockerd SIGKILL, live-restore off | exited 137, HasBeenManuallyStopped=true |
restarted |
| B. clean dockerd restart (SIGTERM, wait), live-restore off | restarted | restarted |
| C. dockerd SIGKILL 6 s into shutdown (container ignores SIGTERM, stop-timeout 20 s), live-restore off | exited 137, HasBeenManuallyStopped=true |
restarted |
| D. dockerd SIGKILL, live-restore on | still running, same PID | still running, same PID |
The daemon logs the stop during restore:
level=debug msg="shutting down container considered alive by containerd" container=d2d12ab9… paused=false restarting=false running=true
Reproduction script
#!/bin/sh
# Reproduction: unless-stopped containers after dockerd crash / restart (Docker 29.8.2, containerd 2.3.6)
set -u
log() { echo "[$(date -u +%H:%M:%S)] $*"; }
FLAGS="--containerd=/run/containerd/containerd.sock --storage-driver=vfs --iptables=false --ip6tables=false --bridge=none --debug"
start_dockerd() {
dockerd $FLAGS "$@" >>/var/log/dockerd.log 2>&1 &
for i in $(seq 1 120); do docker info >/dev/null 2>&1 && return 0; sleep 1; done
log "dockerd did not become ready"; tail -40 /var/log/dockerd.log; exit 1
}
graceful_stop() { pid=$(pidof dockerd); kill -TERM "$pid"; while kill -0 "$pid" 2>/dev/null; do sleep 0.5; done; }
crash() { kill -KILL "$(pidof dockerd)"; sleep 1; }
show() {
for c in "$@"; do
id=$(docker inspect -f '{{.Id}}' "$c")
m=$(grep -o '"HasBeenManuallyStopped":[a-z]*' /var/lib/docker/containers/$id/config.v2.json | cut -d: -f2)
printf ' %-12s policy=%-15s status=%-8s exit=%-4s pid=%-6s HasBeenManuallyStopped=%s\n' "$c" \
"$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$c")" "$(docker inspect -f '{{.State.Status}}' "$c")" \
"$(docker inspect -f '{{.State.ExitCode}}' "$c")" "$(docker inspect -f '{{.State.Pid}}' "$c")" "$m"
done
}
pair() { # $1 prefix, rest: extra run args + command
p=$1; shift
docker run -d --network none --name "$p-us" --restart unless-stopped "$@" >/dev/null
docker run -d --network none --name "$p-al" --restart always "$@" >/dev/null
}
containerd >/var/log/containerd.log 2>&1 &
sleep 2
start_dockerd
docker version --format 'Docker {{.Server.Version}}, containerd {{(index .Server.Components 1).Version}}'
docker pull -q busybox:1.37 >/dev/null && log "busybox pulled"
log "=== A. dockerd crash (SIGKILL), live-restore OFF ==="
pair A busybox:1.37 sleep 86400; show A-us A-al
crash; log "dockerd killed; container processes still alive: $(pgrep -f 'sleep 86400' | wc -l)"
start_dockerd; sleep 3; log "after dockerd restart:"; show A-us A-al
log "=== B. graceful dockerd restart (SIGTERM, waits for exit), live-restore OFF ==="
pair B busybox:1.37 sleep 86400
graceful_stop; log "dockerd stopped gracefully"; start_dockerd; sleep 3; log "after dockerd restart:"; show B-us B-al
log "=== C. dockerd killed mid-shutdown (like systemd TimeoutStopSec), live-restore OFF ==="
pair C --stop-timeout 20 busybox:1.37 sh -c 'trap "" TERM; while true; do sleep 1; done'
kill -TERM "$(pidof dockerd)"; sleep 6; crash; log "dockerd SIGKILLed 6 s into shutdown (containers ignore SIGTERM, stop-timeout 20 s)"
start_dockerd; sleep 3; log "after dockerd restart:"; show C-us C-al
log "=== D. dockerd crash (SIGKILL), live-restore ON ==="
graceful_stop; start_dockerd --live-restore; docker rm -f A-us A-al B-us B-al C-us C-al >/dev/null 2>&1
pair D busybox:1.37 sleep 86400; show D-us D-al
crash; start_dockerd --live-restore; sleep 3; log "after dockerd restart:"; show D-us D-al
log "=== daemon log lines about restore ==="
grep -E 'shutting down container considered alive|Loading containers: (start|done)' /var/log/dockerd.log | sed 's/^/ /' | tail -15
Full output
Docker 29.8.2, containerd v2.3.6
[17:47:46] busybox pulled
[17:47:46] === A. dockerd crash (SIGKILL), live-restore OFF ===
A-us policy=unless-stopped status=running exit=0 pid=187 HasBeenManuallyStopped=false
A-al policy=always status=running exit=0 pid=243 HasBeenManuallyStopped=false
[17:47:51] dockerd killed; container processes still alive: 2
[17:48:07] after dockerd restart:
A-us policy=unless-stopped status=exited exit=137 pid=0 HasBeenManuallyStopped=true
A-al policy=always status=running exit=0 pid=533 HasBeenManuallyStopped=false
[17:48:08] === B. graceful dockerd restart (SIGTERM, waits for exit), live-restore OFF ===
[17:48:21] dockerd stopped gracefully
[17:48:27] after dockerd restart:
B-us policy=unless-stopped status=running exit=0 pid=1081 HasBeenManuallyStopped=false
B-al policy=always status=running exit=0 pid=1057 HasBeenManuallyStopped=false
[17:48:28] === C. dockerd killed mid-shutdown (like systemd TimeoutStopSec), live-restore OFF ===
[17:48:36] dockerd SIGKILLed 6 s into shutdown (containers ignore SIGTERM, stop-timeout 20 s)
[17:49:02] after dockerd restart:
C-us policy=unless-stopped status=exited exit=137 pid=0 HasBeenManuallyStopped=true
C-al policy=always status=running exit=0 pid=1822 HasBeenManuallyStopped=false
[17:49:03] === D. dockerd crash (SIGKILL), live-restore ON ===
D-us policy=unless-stopped status=running exit=0 pid=2504 HasBeenManuallyStopped=false
D-al policy=always status=running exit=0 pid=2560 HasBeenManuallyStopped=false
[17:49:35] after dockerd restart:
D-us policy=unless-stopped status=running exit=0 pid=2504 HasBeenManuallyStopped=false
D-al policy=always status=running exit=0 pid=2560 HasBeenManuallyStopped=false
[17:49:37] === daemon log lines about restore ===
time="2026-10-29T17:47:51.830970074Z" level=debug msg="shutting down container considered alive by containerd" container=d2d12ab9381bfe111ef3d81f0848075d1e57b3c594d68ccbbcfd9bd14af2f403 paused=false restarting=false running=true
time="2026-10-29T17:48:03.547977366Z" level=info msg="Loading containers: done."
time="2026-10-29T17:48:22.241021412Z" level=info msg="Loading containers: start."
time="2026-10-29T17:48:23.319430308Z" level=info msg="Loading containers: done."
time="2026-10-29T17:48:37.787761376Z" level=info msg="Loading containers: start."
time="2026-10-29T17:48:37.897514583Z" level=debug msg="shutting down container considered alive by containerd" container=e001e0be5f8c8942e74d11cdfe712d1165490f0ff59243d70f725dcea4181b53 paused=false restarting=false running=true
time="2026-10-29T17:48:37.898568132Z" level=debug msg="shutting down container considered alive by containerd" container=66a413eb24575a727196dd6371c8198bc2c25fefdbfe0e2dce93f7130098a9c4 paused=false restarting=false running=true
time="2026-10-29T17:48:37.915650455Z" level=debug msg="shutting down container considered alive by containerd" container=bffeb038730d0d377d0b71011a9b2916bf0bdd8a732cdf4a501b552e24802ac4 paused=false restarting=false running=true
time="2026-10-29T17:48:37.917730223Z" level=debug msg="shutting down container considered alive by containerd" container=9b4ce6b32f07f5a28e6768ea714d65d057a0326ede38b3b5ec7a5bf4e4e8e52d paused=false restarting=false running=true
time="2026-10-29T17:48:37.927318991Z" level=debug msg="shutting down container considered alive by containerd" container=1d1d00ab7af11c871bd235eaa273034ffffeb8586e6db373fe292ac60e6f93af paused=false restarting=false running=true
time="2026-10-29T17:48:58.967052265Z" level=info msg="Loading containers: done."
time="2026-10-29T17:49:24.137523014Z" level=info msg="Loading containers: start."
time="2026-10-29T17:49:25.231805147Z" level=info msg="Loading containers: done."
time="2026-10-29T17:49:31.405805634Z" level=info msg="Loading containers: start."
time="2026-10-29T17:49:31.469102953Z" level=info msg="Loading containers: done."
Expected behavior
A stop that the daemon issues itself during restore(), for a container that survived an unclean daemon exit, should not count as a manual stop. unless-stopped containers should then be restarted, as they are after a clean daemon restart.
One possible approach: in the restore path, keep the container's previous HasBeenManuallyStopped value, or stop it through a path that does not set the flag. I have not checked whether other callers rely on the current behaviour.
Impact
We hit this in production. dockerd 29.6.1 aborted with BuildKit's concurrent map iteration and map write (moby/buildkit#6915, fixed in 29.7.0), and systemd restarted it about 30 s later. Two unless-stopped application containers stayed down, one of them for three weeks. Coolify, for example, uses unless-stopped for every application container it manages.
Scenario C suggests a second trigger. A package-driven or manual systemctl restart docker can hit the same path when a container's stop timeout is longer than systemd's stop timeout. The shipped docker.service does not set TimeoutStopSec, so systemd's default (90 s) applies, while the daemon's own shutdown timeout follows the containers' stop timeouts.
Related
- docker/for-linux#652: the same symptom, with a comment describing a daemon crash.
- #11065, #41302, #47792, and PR #53415: the same flag set by
docker killwith non-terminating signals. That is a different trigger, and #53415 would not cover this path, because restore uses the container's stop signal. - #41817: an earlier change in the same restore code.
docker version / docker info
Output from the reproduction environment (docker:29.8.2-dind under rootless Podman, containerd started separately). Production is Ubuntu 26.04.1, kernel 7.0.0-38, Docker CE packages 29.8.2 / containerd.io 2.3.6. The incident happened on 29.6.1; the code path is the same on 29.8.2.
docker version / docker info
$ docker version
Client:
Version: 29.8.2
API version: 1.56
Go version: go1.26.8
Git commit: 7fc2dff
Built: Wed Sep 30 19:31:45 2026
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 29.8.2
API version: 1.56 (minimum version 1.40)
Go version: go1.26.8
Git commit: 8af9fe3
Built: Wed Sep 30 19:34:22 2026
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v2.3.6
GitCommit: ee2735368117d2eb259779949d5e75cdafec9761
runc:
Version: 1.5.2
GitCommit: v1.5.2-0-g29dd3dc
docker-init:
Version: 0.19.0
GitCommit: de40ad0
$ docker info
Client:
Version: 29.8.2
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.37.2
Path: /usr/local/libexec/docker/cli-plugins/docker-buildx
compose: Docker Compose (Docker Inc.)
Version: v5.6.0
Path: /usr/local/libexec/docker/cli-plugins/docker-compose
Server:
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 29.8.2
Storage Driver: vfs
Logging Driver: json-file
Cgroup Driver: cgroupfs
Cgroup Version: 2
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
CDI spec directories:
/etc/cdi
/var/run/cdi
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version: ee2735368117d2eb259779949d5e75cdafec9761
runc version: v1.5.2-0-g29dd3dc
init version: de40ad0
Security Options:
seccomp
Profile: builtin
cgroupns
Kernel Version: 7.0.0-38-generic
Operating System: Alpine Linux v3.24
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 14.97GiB
Name: e3d850c20b2d
ID: 8d8791e0-6c75-4e48-9d81-bc0fdeb492f0
Docker Root Dir: /var/lib/docker
Debug Mode: false
Experimental: false
Insecure Registries:
127.0.0.0/8
::1/128
Live Restore Enabled: false
Product License: Community Engine
Firewall Backend: iptables
EnableUserlandProxy: true
UserlandProxyPath: /usr/local/bin/docker-proxy
- Dominant language
- Go
- Stars
- 72.2k
- Forks
- 19.3k
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 157
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from moby/moby
-
hack/make: copy_binaries fails when only /usr/local/bin/runc is presentPossibly taken @aryansk claimed this 80 days ago. Openkind/bug
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
moby/moby#53442 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
Bug in checking nDotsSet, should be nDotsSet and > 0, leads to failed container name resolution in DinDPossibly taken @NavneetNayak claimed this 115 days ago. Openkind/bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
moby/moby#53104 · 2 comments ·
Maintainers usually reply within 1 day
-
Endpoint (container) name is not passed to network-driver plugins at CreateEndpoint (unlike IPAM drivers, #50586)Possibly taken @claymore666 claimed this 150 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 1 day
-
Clarify target terminal for command examples in `docs/contributing/set-up-dev-env.md`May be free again A pull request for this issue was closed without being merged. Openkind/bug status/0-triage
Difficulty 1/5 Under an hour Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
Default seccomp policy does not allow calling fanotify_init without CAP_SYS_ADMINPossibly taken @ychampion claimed this 123 days ago. Openarea/security/seccomp help wanted kind/enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
moby/moby#49756 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
Similar issues
-
backlog
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
EchoTools/nevr-runtime#2385 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 5 days
-
项目日志弹出窗尺寸可拉伸功能Open
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Maintainers usually reply within 1 day
-
bug good first issue priority: P2 status: ready
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
RhinoLink-Tech/data-fabric#502 ·
Maintainers usually reply within 1 day
-
sweep
Difficulty 1/5 1-3 hours Newbie friendliness 88/100
bketelsen/truenas-mcp#65 ·
Maintainers usually reply within 1 day