Deepening your understanding of the container lifecycle: the Created, Running, Paused, Stopped, Exited, and Dead state machine along with the commands that control it; inspecting JSON metadata with docker inspect and jq, real-time monitoring with top, stats, and events, and the meaning of exit codes.

After running our first containers in episode 3 and mastering the CLI fundamentals — docker run, ps, stop, start, rm, exec, logs, and port mapping — in this episode we go deeper: understanding the container lifecycle in full and how to manage its state.
Why does this matter? Because in the production world, containers rarely just "run and finish". Containers get restarted during deployment, paused during maintenance, crash and enter a restart loop, or hang in an unclear status. An engineer who doesn't understand the container state machine will panic at the sight of an Exited container — when it could be perfectly normal. Conversely, an engineer who understands state can read a single line of docker ps and immediately know the condition of the system: which containers are running, which are stuck in a restart loop, and which are truly dead.
Episode 4 is the bridge from "running containers" to "managing a production system". We'll dissect the container state machine (Created, Running, Paused, Stopped, Exited, Dead, and Restarting), the state control commands (create, start, pause, unpause, restart, kill), JSON metadata inspection with docker inspect, real-time monitoring (top, stats, events), the meaning of exit codes, and close with safe system cleanup (system prune, system df).
Every container always exists in one of the following states. Understand this like a map: every command you run is a "move" from one point to another.
create start
(no) ─────────► Created ───────────► Running
│ │
pause/ │ │ stop / (proses keluar)
unpause │ │
▼ ▼
Paused Stopped / Exited
│ │ rm
└──────────► (hilang)| State | Meaning | How to Enter |
|---|---|---|
| Created | Configuration stored, process not yet running | docker create |
| Running | Main process running | docker start, docker run |
| Paused | Process frozen (SIGSTOP), memory retained | docker pause |
| Stopped / Exited | Process stopped, state + filesystem retained | docker stop, process exits on its own |
| Restarting | Being restarted (crash loop) | restart policy / docker restart |
| Dead | Cannot be stopped/deleted normally; needs intervention | abnormal condition, process lost |
Important distinctions to remember:
docker ps shows Exited for containers that have stopped. The difference is subtle: Stopped is the "official" state resulting from the stop command, while Exited is the final status of a process that has finished (whether intentionally or by crashing).STATUS PORTS NAMES
Up 3 minutes 0.0.0.0:8080->80 my-web
Exited (0) ... old-jobAll the following commands only move the state — they don't create a new image. Memorize the transitions:
docker create --name my-web nginx
docker ps -a --filter "status=created"docker create stores the container's entire configuration (image, ports, env, mounts) without running the process. Useful for preparing a container in advance and running it at the right time — and for checking the configuration before execution.
docker start my-web
docker pause my-web
docker unpause my-web
docker restart my-webNote docker restart on the last line — it's a combination of stop + start with a SIGTERM grace period. Meanwhile docker kill below is the opposite of stop: it sends SIGKILL immediately without giving the process a chance to clean up:
docker kill my-webWarning
Understand the shutdown hierarchy: docker stop sends SIGTERM (the process can save state and close connections cleanly) then SIGKILL after the grace period; docker kill immediately sends SIGKILL (the process is force-stopped, with no chance to clean up). For databases or applications that persist data, always prefer stop — kill is only for processes that no longer respond.
The Restarting state above often causes confusion: is it a normal state or a sign of a crash loop? The answer lies in the restart policy — the rule that determines what the daemon does when a container stops. This rule is set when the container is created:
docker run -d --name my-web --restart always nginx
docker inspect my-web | jq '.[0].HostConfig.RestartPolicy'{
"Name": "always",
"MaximumRetryCount": 0
}| Policy | Behavior | When to Use |
|---|---|---|
no | (default) Never restarted automatically | One-off jobs, experiments |
on-failure[:max] | Restarts only if the exit code isn't 0, at most N times | Services that fail temporarily; avoid infinite loops |
unless-stopped | Restarts automatically, unless stopped manually by the user | Services that must always run on the host |
always | Always restarts, including after a daemon restart | Production services that require uptime |
Important
When you see a container with status Restarting (or Restarting (1) 5 seconds ago) in docker ps, it means the restart policy is working because the main process exited — usually because of a crash. This isn't a Docker bug, it's a signal that the application is failing repeatedly. Don't immediately add a new policy; first read docker logs to find out why the application is crashing. A restart policy holds the door open — but a door left open for a broken application is just a disguised crash loop.
docker inspect is the gateway to all of a container's metadata in JSON form — the most important tool for understanding a container's true condition:
docker inspect my-webThe output is long, so get in the habit of filtering it with jq (or --format). The four fields you'll read most often:
docker inspect my-web | jq '.[0].Id'
docker inspect my-web | jq '.[0].State'
docker inspect my-web | jq '.[0].Config.Image'
docker inspect my-web | jq '.[0].Mounts'
docker inspect my-web | jq '.[0].NetworkSettings.IPAddress'{
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 12345,
"ExitCode": 0,
...
}| Field | Diagnostic Function |
|---|---|
Id | The container's full ID (not the shortened one in ps) |
State | Current status: Running, Paused, Restarting, OOMKilled, Pid, ExitCode |
Config | Configuration at creation: image, Env, Cmd, Entrypoint, Hostname |
Mounts | Volumes and bind mounts: Source, Destination, RW mode |
NetworkSettings | Internal IP, port bindings, the network in use |
A typical scenario: a container stops for no apparent reason. docker inspect will answer — whether OOMKilled: true (out of memory), what the ExitCode is, and what Error is recorded. That's far faster than guessing.
Every container stops with an exit code — the number recorded from its main process. This is the container's "final message":
| Exit Code | Meaning |
|---|---|
0 | Normal exit, success |
1 | Generic error / application failure |
2 | Usage error — often from the shell |
127 | Command not found (command not found) |
130 | Terminated by SIGINT (Ctrl+C) |
137 | SIGKILL — often due to OOM (137 = 128 + 9) |
139 | Segmentation fault (128 + 11, SIGSEGV) |
docker run --name fail alpine sh -c "exit 1"
docker inspect fail | jq '.[0].State.ExitCode'
docker run --name oom-test -m 20m alpine sh -c "tail /dev/zero"The last example above deliberately runs a container that forces an OOM (reading /dev/zero endlessly with a 20 MB memory limit). Note the exit code 137 — the most common signal that a container was killed for running out of memory. Learning to read exit codes is the first debugging skill that will save you in production.
docker top my-webdocker top shows the processes running inside the container (from the host's perspective). Compare it with ps aux on the host — the nginx container appears as an nginx process with its host user and PID.
docker statsdocker stats is a live dashboard: CPU %, memory usage/limit, network I/O, and block I/O columns for all active containers — updated every few seconds. An essential tool when figuring out "which container is eating memory".
docker eventsdocker events streams all daemon events in real time: create, start, stop, die, restart, pause, kill, exec_start, and others. Run this in one terminal, then try docker run -d --rm alpine sleep 60 in another — you'll see the create, start, and die events flowing. It's the purest observation tool for studying the lifecycle we just discussed.
Tip
For debugging a crash loop, the ultimate combination: docker inspect <id> | jq '.[0].State.ExitCode' for the status, docker logs <id> --tail 50 for the last messages, and docker stats for resource usage patterns before the crash. The three answer three questions: why it died, what the application said, and what happened before.
Unused containers, images, volumes, and networks pile up — and fill up the disk at /var/lib/docker. These two commands are your cleanup crew:
docker system dfdocker system df reports how much disk is used by images, containers, volumes, and build cache — plus the RECLAIMABLE column (how much can be freed).
docker system prune
docker system prune -a
docker system prune -a --volumesUnderstand each option carefully:
docker system prune — removes stopped containers, unused networks, dangling images, and build cache.-a (all) — always asks first and removes all images not used by active containers, not just dangling ones.--volumes — removes unused volumes. Volumes contain data — this command is dangerous for data that isn't backed up.WARNING! This will remove all dangling images...
Are you sure you want to continue? [y/N]Warning
docker system prune -a --volumes is the most destructive combination — it removes unused images and all unused volumes, including forgotten database data. Before running this option on a production machine, make sure (1) there are no stopped containers you still need, and (2) the unused volumes are truly safe to discard. When in doubt: run it without --volumes, or back up first.
Assuming Exited always means error. A hello-world container that's Exited (0) is normal behavior. Read the exit code before panicking — 0 means it finished successfully.
docker rm on a still-running container. The error is "cannot remove running container". Stop it first (stop) or use -f deliberately.
Not reading State.Error on crash. docker inspect contains the error message in JSON — often the answer is already there before you start guessing.
Skipping docker system df. A full disk on a Docker server usually isn't caused by used images but by accumulated build cache and idle containers/images. Regular audits with system df prevent a disk crisis.
Running system prune -a --volumes without thinking. Data that isn't backed up can be lost permanently. Always confirm what's about to be deleted.
In this episode 4 you've deepened your understanding of the container lifecycle: the state machine from Created → Running → Paused → Stopped/Exited → Dead (plus Restarting), the state control commands (create, start, pause, unpause, restart, kill), reading JSON metadata with docker inspect and jq (the State, Config, Mounts, NetworkSettings fields), real-time monitoring with docker top, docker stats, and docker events, the meaning of exit codes (especially 137 for OOM), and system cleanup with docker system df and docker system prune along with the dangers of the -a --volumes option.
Core takeaways:
pause freezes the process; stop (SIGTERM) stops cleanly; kill (SIGKILL) force-stops.docker inspect + jq are your eyes — State.ExitCode, OOMKilled, and State.Error answer why a container died.137 = killed (often OOM); 0 = finished normally — read before panicking.system df for disk audits; system prune for cleanup; be careful with --volumes.Now you're no longer just "running containers" — you're managing their lifecycle. In the next episode, episode 5, we'll level up: building custom images with a Dockerfile — understanding layered architecture and the build context, the core instructions FROM, WORKDIR, COPY vs ADD, RUN, EXPOSE, and the fundamental difference between CMD and ENTRYPOINT that will change how you package your own applications. See you in episode 5!