Skip to main content
RunBook Academy

Docker & ContainersVI · Container LifecycleContainer lifecycle

Container lifecycle, signals, and graceful shutdown

Foundation⏱ ~20 mindockerbash

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

Not yet marked complete on this device.

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.

A production-shaped docker run invocation
$ 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 
  1. 01dockerThe Docker CLI. Talks to the daemon over the configured context (default: local socket).
  2. 02runCreate + start in one step. Equivalent to `docker create` followed by `docker start`.
  3. 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.
  4. 04--memory 512mHard memory limit. Enforced via the memory cgroup. Container is OOM-killed if it exceeds this.
  5. 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.
  6. 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`.
  7. 07--stop-timeout 30Seconds to wait after SIGTERM before SIGKILL. Give long-running requests time to drain.
  8. 08--health-cmd "curl -fs http://localhost/health"Healthcheck command. Exit 0 = healthy, 1 = unhealthy, 2 = reserved.
  9. 09--health-interval 10sHow often to run the healthcheck.
  10. 10--health-start-period 30sGrace period before healthchecks count toward `--health-retries`. Critical for slow-start apps.
  11. 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. 12--tmpfs /tmp:rw,size=64mWritable tmpfs mounted at /tmp, capped at 64 MiB. Disappears when the container stops.
  13. 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. 14--labelKey=value metadata. Used by Compose, log drivers, reverse proxies, and Prometheus relabel rules.
  15. 15-dDetached. Returns immediately. Combine with `docker logs -f` to follow output.
  16. 16nginx: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.27

Configuration: 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.

daemon.jsonDefault-aware daemon config for production hosts
"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"
  1. 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.

  2. 02"live-restore"= true

    Keep 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".

  3. 03"userland-proxy"= false

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

  4. 04"no-new-privileges"= true

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

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

  6. 06"log-opts.max-size"= "10m"

    Per-log-file size cap before rotation.

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

  8. 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:

Configuration changeinit
docker run --init --name web -d nginx:1.27

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

Read-only / Safefind containers that are running and useless
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:01Z

Illustrative 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, and health=none means 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.

Read-only / Safewhy is it unhealthy?
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

SymptomLikely causeWhere to look
Container takes 10 s to stopNo signal handler in PID 1; missing --initdocker inspectState.Error after the kill; inspect the ENTRYPOINT
Container immediately restarts after docker stopRestart policy is always, not unless-stoppeddocker inspectHostConfig.RestartPolicy.Name
OOM-killed containerMemory limit too low or --memory-swap set to -1journalctl -u docker; dmesg; State.OOMKilled: true
Healthcheck always “starting”Healthcheck script returns non-zero or the start period is too shortdocker inspectState.Health.Log
Up N days (unhealthy) and never restartedRestart policies react to PID 1 exiting, not to health status. Nothing on plain Docker consumes Health.Statusdocker inspect.State.Health.FailingStreak and .RestartCount

Knowledge check

Knowledge check · 6 questions

  1. Q1. What does `docker stop` send to PID 1 inside the container by default?

  2. Q2. Adding `--init` to `docker run` makes the container init reap zombie processes and forward signals to the entrypoint.

  3. Q3. Which restart policies will restart a container after a host reboot? Select all that apply.

  4. Q4. What is the recommended way to make a graceful shutdown succeed when your entrypoint is a shell script that does not trap SIGTERM?

  5. Q5. A container shows `Up 6 days (unhealthy)` with `--restart unless-stopped` and RestartCount 0. Why has Docker not restarted it?

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