Container Lifecycle
Measured on Docker Desktop 4.81.0 / Engine 29.6.1 (macOS, linux/arm64), 2026-09-30.
Running a container
Section titled “Running a container”docker run is the most common command you will use. It pulls the image if needed, creates a container, and starts it.
docker run nginx:1.31-alpineThis runs nginx in the foreground — your terminal attaches to its output. Press Ctrl+C to stop it.
Detached mode with -d
Section titled “Detached mode with -d”Add -d to run the container in the background:
docker run -d nginx:1.31-alpineDocker prints the full 64-character container ID and returns your prompt immediately. docker ps shows the first 12 characters (docker ps --no-trunc shows all 64).
Naming a container with --name
Section titled “Naming a container with --name”By default Docker assigns a random name. Use --name to give it a predictable one:
docker run -d --name webserver nginx:1.31-alpineNames are unique per engine. That matters later: a stopped container keeps its name.
Listing containers
Section titled “Listing containers”# Only running containersdocker ps
# All containers — running AND stoppeddocker ps -aCONTAINER ID IMAGE STATUS NAMESc0d6336221c2 nginx:1.31-alpine Up 2 minutes webserverb7c8d9e0f1a2 alpine:3.24 Exited (0) 5 minutes ago old-taskThe STATUS column tells you whether the container is running (Up) or stopped (Exited, with the exit code of the main process in brackets).
Stopping, starting, and restarting
Section titled “Stopping, starting, and restarting”# Graceful stop — sends SIGTERM, waits 10 s, then SIGKILLdocker stop webserver
# Start a stopped containerdocker start webserver
# Restart (stop then start)docker restart webserverdocker stop does not remove the container. It remains on disk in the stopped state. docker start runs the same command again in the same container. Proof with a command that prints a timestamp (docker start -a attaches to the output):
$ docker run --name t alpine:3.24 sh -c 'echo run at $(date +%s)'run at 1790756753$ docker start -a trun at 1790756754The writable layer survives stop and start
Section titled “The writable layer survives stop and start”A container’s changes live in a writable layer on top of the image. docker stop ends the process; it does not touch that layer. Proven: the container writes a small file and a 50 MB file, is stopped, started again, and both are still there:
$ docker run -d --name w alpine:3.24 sh -c 'echo hi > /f.txt; dd if=/dev/zero of=/big.bin bs=1M count=50; sleep 300'$ docker diff wA /big.binA /f.txt$ docker ps -as --filter name=w --format 'table {{.Names}}\t{{.Status}}\t{{.Size}}'NAMES STATUS SIZEw Up 1 second 52.4MB (virtual 61.1MB)$ docker stop -t 1 w$ docker ps -as --filter name=w --format 'table {{.Names}}\t{{.Status}}\t{{.Size}}'NAMES STATUS SIZEw Exited (137) Less than a second ago 52.4MB (virtual 61.1MB)$ docker start w && docker exec w cat /f.txthi$ docker exec w ls -l /big.bin | awk '{print $5}'52428800docker diff lists changes against the image (A added, C changed, D deleted). SIZE is the writable layer (52.4MB), virtual adds the shared read-only image (61.1MB). After docker stop the size is still there. The layer disappears with docker rm (docker ps -as --filter name=w -q | wc -l printed 0).
Exit codes
Section titled “Exit codes”The Exited (N) number is the exit code of the main process, and it is a useful diagnostic. All of these were produced here:
| Code | How it happened | Meaning |
|---|---|---|
0 | docker run alpine:3.24 true | Success |
1–125 | sh -c 'exit 42' printed 42; exit 3 printed 3 | The application’s own exit status |
125 | docker run --name web … with the name already in use | The Docker daemon itself failed to run the container |
126 | docker run alpine:3.24 /etc → exec: "/etc": is a directory: permission denied | Command found but cannot be executed |
127 | docker run alpine:3.24 nosuchcmd → exec: "nosuchcmd": executable file not found in $PATH | Command not found |
137 | docker stop on a sleep PID 1 after the 10 s timeout; docker rm -f | 128 + 9: SIGKILL (also what an out-of-memory kill looks like) |
143 | docker stop on sleep run with --init | 128 + 15: the process died from SIGTERM |
Removing a container
Section titled “Removing a container”# Remove a stopped containerdocker rm webserver
# Force-remove a running container (sends SIGKILL)docker rm -f webserverWithout -f, removing a running container is refused:
$ docker rm webError response from daemon: cannot remove container "web": container is running: stop the container before removing or force removedocker rm -f kills with SIGKILL straight away (no SIGTERM, no grace period): time docker rm -f on a sleep container took 0.16 s, whereas docker stop on the same container takes the full 10 s. Use stop when the process needs to flush data, rm -f when it does not.
Auto-cleanup with --rm
Section titled “Auto-cleanup with --rm”For one-shot tasks you never want to manage manually, add --rm. The container is removed automatically the moment its process exits.
docker run --rm alpine:3.24 echo "Hello, Docker!"Hello, Docker!The container is gone as soon as the echo finishes — no docker rm needed. Proven: docker stop on a --rm container followed by docker ps -a -q --filter name=… printed nothing.
# 1. Run nginx detached with a name
docker run -d --name webserver nginx:1.31-alpine
# 2. List running containers
docker ps
# 3. Stop it
docker stop webserver
# 4. Show all containers — notice STATUS is Exited
docker ps -a
# 5. Start it again
docker start webserver
# 6. Remove it (stop first)
docker stop webserver && docker rm webserver
# 7. One-shot: auto-cleanup with --rm
docker run --rm alpine:3.24 echo "done — already gone"Reading AI-generated code
Section titled “Reading AI-generated code”An AI-written “free up disk space on the build agent” step:
🐛 Spot the bug
#!/bin/sh# AI: "stop all containers to free disk space before the next job"docker stop $(docker ps -q)df -h /var/lib/dockerShow the answer
docker stop only ends processes. The writable layers stay, so the disk usage does not move. From the proof above (one container that wrote 50 MB):
Running: Up 1 second 52.4MB (virtual 61.1MB)Stopped: Exited (137) Less than a second ago 52.4MB (virtual 61.1MB) # nothing reclaimedRemoved: (docker ps -as --filter name=w -q | wc -l -> 0) # goneThe script also has a second bug: docker ps -q lists only running containers, so it never looks at the exited ones that hold most of the space. And with nothing running, $(docker ps -q) is empty and docker stop fails with "docker stop" requires at least 1 argument (exit 1). Remove the containers you own, selected by a label instead of by “everything”:
#!/bin/shset -eu# Remove only containers this job created (labelled at run time with --label job=ci)ids=$(docker ps -aq --filter label=job=ci)[ -z "$ids" ] || docker rm -f $idsGlobal cleanup commands such as docker container prune exist, but on a shared engine they delete other people’s stopped containers too. Filter (--filter label=…, --filter until=24h) or remove by name.
Mental Model
Section titled “Mental Model”A container has a process lifecycle (created → running → exited) and a separate existence lifecycle (exists until docker rm). stop, start, restart, pause move the process; only rm ends the existence. Data in the writable layer follows the existence, not the process. That is why a stopped container still costs disk and still owns its name, and why deleting one is the moment its unmounted files really go.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Run in the foreground (no -d) | See logs in real time as soon as it runs, easy to stop with Ctrl+C | Blocks the terminal — you cannot use it for anything else until it stops |
Run detached (-d) | Terminal stays free, easy to run several containers at once | Need docker logs to see output, and it’s easy to forget a container is running in the background |
docker stop | Process can flush and exit cleanly (exit 0) | Waits up to 10 s if PID 1 ignores SIGTERM |
docker rm -f | Instant (0.16 s here) | SIGKILL: no graceful shutdown, exit 137 |
Common mistakes
Section titled “Common mistakes”- Running a container without
-dand having it block the terminal by accident, forcing you to open a new one to keep working. - Running
docker stopand assuming the container is gone, when it is still on disk in a stopped state — forgettingdocker rmuntil they pile up. - Reaching for
docker restartthe moment something looks wrong, without checkingdocker logsfirst to see why it crashed, and ending up fixing the same problem over and over. - Reading exit code 137 as “the app crashed” instead of “something sent SIGKILL” (the stop timeout,
rm -f, or the out-of-memory killer).
The same idea exists one level up: Kubernetes decides per Pod what to do when a container exits with its restartPolicy (Always, OnFailure, Never), as described in the Pod lifecycle docs; the kubelet restarts the container, not a person typing docker restart.
Common Misconceptions
Section titled “Common Misconceptions”- “
docker stopfrees the disk space.” It frees CPU and memory. The writable layer (52.4MB in the proof) is released only bydocker rm. - “
docker startcreates a fresh container.” It runs the same container again: same ID, same writable layer, same command (run at …753then…754). - “
--rmdeletes the image.” It deletes the container only. - “Exit code 125/126/127 come from my app.” They come from Docker or the shell: 125 daemon error, 126 not executable, 127 not found.