Docker & ContainersVI · Container LifecycleContainer lifecycle
Container lifecycle, signals, and graceful shutdown
What you'll learn
- Explain how a container transitions through create / start / stop / kill
- Understand why PID 1 inside a container matters for signal handling
- Configure graceful shutdowns with stop-timeout and healthchecks
- Recognise common shutdown-related failure modes
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-09
A container is not a magic black box. It is a Linux process tree, owned by the Docker daemon, executed by an OCI runtime, with explicit lifecycle transitions. Every operational decision you make — restart policies, health checks, graceful shutdowns, dependency ordering — comes back to this.
This lesson walks through the lifecycle with annotated commands, then shows the production implications.
The lifecycle, in order
Annotated command
The most important command in Docker is docker run. Most flags have
operational consequences; here is the production-relevant breakdown.
$ dockerrun--name web--memory 512m--cpus 1.5--restart unless-stopped--stop-timeout 30--health-cmd "curl -fs http://localhost/health"--health-interval 10s--health-start-period 30s--read-only--tmpfs /tmp:rw,size=64m-p 8080:80--label-dnginx:1.27 - 01
dockerThe Docker CLI. Talks to the daemon over the configured context (default: local socket). - 02
runCreate + start in one step. Equivalent to `docker create` followed by `docker start`. - 03
--name webStable name. Without this, the daemon assigns a random adjective_noun pair. Names matter for DNS, logging filters, and `docker rm` scripts.⚠ Two containers with the same name fail to start — the second `docker run --name web` errors out. Always script a unique name or use `docker rm` first. - 04
--memory 512mHard memory limit. Enforced via the memory cgroup. Container is OOM-killed if it exceeds this. - 05
--cpus 1.5CPU quota. Equivalent to `--cpu-quota=150000 --cpu-period=100000`. Container can never use more than 1.5 cores worth of CPU time. - 06
--restart unless-stoppedRestart policy. Other options: `no`, `always`, `on-failure[:max-retries]`. `unless-stopped` is the production default.⚠ `always` will restart a container even after `docker stop` — confusing during maintenance. Prefer `unless-stopped`. - 07
--stop-timeout 30Seconds to wait after SIGTERM before SIGKILL. Give long-running requests time to drain. - 08
--health-cmd "curl -fs http://localhost/health"Healthcheck command. Exit 0 = healthy, 1 = unhealthy, 2 = reserved. - 09
--health-interval 10sHow often to run the healthcheck. - 10
--health-start-period 30sGrace period before healthchecks count toward `--health-retries`. Critical for slow-start apps. - 11
--read-onlyMount the container root filesystem read-only. Forces explicit `tmpfs` mounts for writeable paths.⚠ Many legacy images assume a writable `/tmp` and will fail in mysterious ways. Combine with `--tmpfs /tmp:rw,size=64m` or a named volume. - 12
--tmpfs /tmp:rw,size=64mWritable tmpfs mounted at /tmp, capped at 64 MiB. Disappears when the container stops. - 13
-p 8080:80Publish host port 8080 to container port 80. Implemented as an iptables DNAT rule plus a docker-proxy process; `--userland-proxy` defaults to true.⚠ A published port is DNAT’d before the packet reaches the INPUT chain, so ufw rules never see it. Bind the publish (`-p 127.0.0.1:8080:80`) rather than relying on the host firewall. - 14
--labelKey=value metadata. Used by Compose, log drivers, reverse proxies, and Prometheus relabel rules. - 15
-dDetached. Returns immediately. Combine with `docker logs -f` to follow output. - 16
nginx:1.27Image reference. Pinned by tag (and ideally by digest: `nginx:1.27@sha256:…`).
Show reconstructed command
docker run --name web --memory 512m --cpus 1.5 --restart unless-stopped --stop-timeout 30 --health-cmd "curl -fs http://localhost/health" --health-interval 10s --health-start-period 30s --read-only --tmpfs /tmp:rw,size=64m -p 8080:80 --label -d nginx:1.27Configuration: the daemon side
Most lifecycle behaviour can be controlled per-container (as above) or
centrally via the daemon’s /etc/docker/daemon.json. The platform ships a
default; production hosts override it.
"default-runtime": "runc"
"live-restore": true
"userland-proxy": false
"no-new-privileges": true
"log-driver": "json-file"
"log-opts.max-size": "10m"
"log-opts.max-file": "3"
"storage-driver": "overlay2"01
"default-runtime"= "runc"OCI runtime to use when no per-container override is set. `runc` is the upstream default; `runsc` (gVisor) and `kata` add isolation at a performance cost.
Production: Keep `runc` unless you have a concrete threat model that justifies gVisor/Kata.
02
"live-restore"= trueKeep containers running when the daemon restarts. Reduces disruption during daemon upgrades.
Production: Enable on any host where container uptime matters more than full daemon restart. Validate against your network plugin — bridge networking requires the daemon to recreate rules.
⚠ With `live-restore: true`, daemon config changes still take effect; only running containers survive. Don't confuse "live-restore" with "no-restart".
03
"userland-proxy"= falseDisable the userland proxy process used for port mapping. On modern kernels Docker uses iptables/ipvs directly, which is faster.
Production: Set `false` in production. Reduces per-publish-proxy overhead.
04
"no-new-privileges"= trueEquivalent to `--security-opt no-new-privileges` on every container. Disables setuid binaries from gaining privileges.
Production: Default-on for any untrusted workload. Can be overridden per-container.
05
"log-driver"= "json-file"Default log driver. Each container writes to a JSON file under /var/lib/docker/containers/.
Production: Switch to `journald`, `syslog`, or a Fluent Bit/Vector sidecar for central logging. Otherwise logs fill the disk.
06
"log-opts.max-size"= "10m"Per-log-file size cap before rotation.
07
"log-opts.max-file"= "3"Number of rotated log files to keep.
Production: Total per-container log budget is `max-size * (max-file + 1)`. For 200 containers at 30 MiB that's 6 GB. Plan for it.
08
"storage-driver"= "overlay2"Filesystem layering driver. `overlay2` is the only sensible choice on modern Linux.
Production: Do not use `devicemapper`, `btrfs`, or `vfs`. `overlay2` is the default on kernel 4.0+.
Why PID 1 matters
The conventional fix:
docker run --init --name web -d nginx:1.27Graceful shutdown in production
Health checks vs liveness vs readiness
The HEALTHCHECK directive and --health-cmd flag define a single check: is
the process responding? They are not liveness probes (Kubernetes liveness will
restart the container) or readiness probes (Kubernetes readiness will remove
the pod from the service).
In Docker Compose and a single host, --health-cmd is useful for
docker ps output, docker inspect, and any external orchestrator that
reads State.Health.Status. With multiple replicas behind a reverse proxy,
the reverse proxy should also probe the container — do not rely on Docker
health alone.
docker ps -q | xargs -r docker inspect \
--format '{{.Name}}\thealth={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}\trestarts={{.RestartCount}}\tstarted={{.State.StartedAt}}'/api health=unhealthy restarts=0 started=2026-08-06T09:14:02Z
/web health=healthy restarts=0 started=2026-08-06T09:14:05Z
/worker health=none restarts=17 started=2026-08-12T02:51:38Z
/cache health=healthy restarts=0 started=2026-08-06T09:14:01ZIllustrative output
Three distinct states in one output:
api— unhealthy, zero restarts, started six days ago. Alive and not working. Nothing on this host will act on it.worker— no health check at all and 17 restarts. It is being restarted, so the policy is working; the crash loop is the problem, andhealth=nonemeans you have no way to tell whether the restarts are helping.web,cache— healthy, no restarts. The intended state.
health=none on a production service is its own finding: there is
no check, so docker ps can only ever tell you the process exists.
CONTAINER=api
docker inspect --format '{{json .State.Health}}' "$CONTAINER" | jq '{Status, FailingStreak, Log: (.Log | map({Start, ExitCode, Output}) | .[-2:])}'{
"Status": "unhealthy",
"FailingStreak": 1043,
"Log": [
{
"Start": "2026-08-12T04:12:01Z",
"ExitCode": 1,
"Output": "curl: (28) Operation timed out after 5000 ms\n"
},
{
"Start": "2026-08-12T04:12:31Z",
"ExitCode": 1,
"Output": "curl: (28) Operation timed out after 5000 ms\n"
}
]
}Illustrative output
FailingStreak: 1043 at a 30-second interval is roughly nine hours
of continuous failure on a container that was never restarted. That
number is the cost of the gap this callout is about, and it is
sitting in docker inspect the whole time.
Common failure modes
| Symptom | Likely cause | Where to look |
|---|---|---|
| Container takes 10 s to stop | No signal handler in PID 1; missing --init | docker inspect → State.Error after the kill; inspect the ENTRYPOINT |
Container immediately restarts after docker stop | Restart policy is always, not unless-stopped | docker inspect → HostConfig.RestartPolicy.Name |
| OOM-killed container | Memory limit too low or --memory-swap set to -1 | journalctl -u docker; dmesg; State.OOMKilled: true |
| Healthcheck always “starting” | Healthcheck script returns non-zero or the start period is too short | docker inspect → State.Health.Log |
Up N days (unhealthy) and never restarted | Restart policies react to PID 1 exiting, not to health status. Nothing on plain Docker consumes Health.Status | docker inspect → .State.Health.FailingStreak and .RestartCount |
Knowledge check
Knowledge check · 6 questions
Q1. What does `docker stop` send to PID 1 inside the container by default?
Q2. Adding `--init` to `docker run` makes the container init reap zombie processes and forward signals to the entrypoint.
Q3. Which restart policies will restart a container after a host reboot? Select all that apply.
Q4. What is the recommended way to make a graceful shutdown succeed when your entrypoint is a shell script that does not trap SIGTERM?
Q5. A container shows `Up 6 days (unhealthy)` with `--restart unless-stopped` and RestartCount 0. Why has Docker not restarted it?
Q6. Raising `--stop-timeout` is the correct fix for a container that always takes exactly the full timeout to stop.
Passing score: 75%. Answers are checked in this browser.
Where next
Part XXXI — Docker Networking covers how packets enter and leave a container. The lifecycle decisions made here directly affect how your reverse proxy interacts with the application during a deploy.