Skip to content

Container Lifecycle

Measured on Docker Desktop 4.81.0 / Engine 29.6.1 (macOS, linux/arm64), 2026-09-30.

docker run is the most common command you will use. It pulls the image if needed, creates a container, and starts it.

Terminal window
docker run nginx:1.31-alpine

This runs nginx in the foreground — your terminal attaches to its output. Press Ctrl+C to stop it.

Add -d to run the container in the background:

Terminal window
docker run -d nginx:1.31-alpine

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

By default Docker assigns a random name. Use --name to give it a predictable one:

Terminal window
docker run -d --name webserver nginx:1.31-alpine

Names are unique per engine. That matters later: a stopped container keeps its name.

Terminal window
# Only running containers
docker ps
# All containers — running AND stopped
docker ps -a
CONTAINER ID IMAGE STATUS NAMES
c0d6336221c2 nginx:1.31-alpine Up 2 minutes webserver
b7c8d9e0f1a2 alpine:3.24 Exited (0) 5 minutes ago old-task

The STATUS column tells you whether the container is running (Up) or stopped (Exited, with the exit code of the main process in brackets).

Terminal window
# Graceful stop — sends SIGTERM, waits 10 s, then SIGKILL
docker stop webserver
# Start a stopped container
docker start webserver
# Restart (stop then start)
docker restart webserver

docker 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 t
run at 1790756754

The 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 w
A /big.bin
A /f.txt
$ docker ps -as --filter name=w --format 'table {{.Names}}\t{{.Status}}\t{{.Size}}'
NAMES STATUS SIZE
w 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 SIZE
w Exited (137) Less than a second ago 52.4MB (virtual 61.1MB)
$ docker start w && docker exec w cat /f.txt
hi
$ docker exec w ls -l /big.bin | awk '{print $5}'
52428800

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

The Exited (N) number is the exit code of the main process, and it is a useful diagnostic. All of these were produced here:

CodeHow it happenedMeaning
0docker run alpine:3.24 trueSuccess
1–125sh -c 'exit 42' printed 42; exit 3 printed 3The application’s own exit status
125docker run --name web … with the name already in useThe Docker daemon itself failed to run the container
126docker run alpine:3.24 /etc → exec: "/etc": is a directory: permission deniedCommand found but cannot be executed
127docker run alpine:3.24 nosuchcmd → exec: "nosuchcmd": executable file not found in $PATHCommand not found
137docker stop on a sleep PID 1 after the 10 s timeout; docker rm -f128 + 9: SIGKILL (also what an out-of-memory kill looks like)
143docker stop on sleep run with --init128 + 15: the process died from SIGTERM
Terminal window
# Remove a stopped container
docker rm webserver
# Force-remove a running container (sends SIGKILL)
docker rm -f webserver

Without -f, removing a running container is refused:

$ docker rm web
Error response from daemon: cannot remove container "web": container is running: stop the container before removing or force remove

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

For one-shot tasks you never want to manage manually, add --rm. The container is removed automatically the moment its process exits.

Terminal window
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"

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/docker
Show 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 reclaimed
Removed: (docker ps -as --filter name=w -q | wc -l -> 0) # gone

The 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”:

scripts/free-space.sh
#!/bin/sh
set -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 $ids

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

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.

OptionBenefitCost
Run in the foreground (no -d)See logs in real time as soon as it runs, easy to stop with Ctrl+CBlocks the terminal — you cannot use it for anything else until it stops
Run detached (-d)Terminal stays free, easy to run several containers at onceNeed docker logs to see output, and it’s easy to forget a container is running in the background
docker stopProcess can flush and exit cleanly (exit 0)Waits up to 10 s if PID 1 ignores SIGTERM
docker rm -fInstant (0.16 s here)SIGKILL: no graceful shutdown, exit 137
  • Running a container without -d and having it block the terminal by accident, forcing you to open a new one to keep working.
  • Running docker stop and assuming the container is gone, when it is still on disk in a stopped state — forgetting docker rm until they pile up.
  • Reaching for docker restart the moment something looks wrong, without checking docker logs first 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.

  • “docker stop frees the disk space.” It frees CPU and memory. The writable layer (52.4MB in the proof) is released only by docker rm.
  • “docker start creates a fresh container.” It runs the same container again: same ID, same writable layer, same command (run at …753 then …754).
  • “--rm deletes 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.
What does 'docker stop' send to the container's main process first?
Which command shows ALL containers, including stopped ones?
What does the --rm flag do?
Can you remove a running container without the -f flag?
A container wrote 50 MB into its filesystem, then you ran docker stop. What does docker ps -as show?
docker run alpine nosuchcmd exits with which code, and why?