Chapter 05Lesson 05~130 minutes

Checkpoint Lab — Running Containers: create, run, start, stop, restart, rm, exec, attach, inspect, and Lifecycle State

This checkpoint produces a lifecycle timeline for one digest-identified image and one lab-owned container. You will predict each state transition, capture inspect evidence before and after it, exercise exec and attach without losing the primary process, preserve exit evidence, restart the same object, and prove exact cleanup.

CheckpointState timelineExit evidenceDigest identityCleanup

Learning objectives

  • Build an evidence-backed timeline through created, running, exec, attached/detached, stopped, restarted, exited, and removed states.
  • Predict at least two state changes before issuing the command and compare the prediction with docker inspect evidence.
  • Capture exact image digest, container ID/name, PID, exit code, restart count, labels, timestamps, mounts, networks, logs, and events where applicable.
  • Prove that restarting reuses the same container object while creating a new primary process instance.
  • Perform guarded cleanup that removes only the checkpoint-owned container and preserves unrelated images, volumes, networks, and workloads.
Chapter 05 evidence baseline — verified 2026-09-21. This chapter uses a free/local/disposable Docker path and captures the exact image digest and container ID before lifecycle changes. Version-sensitive behavior is verified against the active daemon instead of assumed. Docker Engine 29.8.1 is the current Engine 29 patch baseline at verification time; Engine 29.7 introduced a daemon-level default-stop-timeout option. Current Docker documentation distinguishes graceful stop from kill/force-removal, exec from attach, and explicit restart from restart policy. Labs target only named/labeled chapter-owned containers and never use broad prune or production workloads.

1. Checkpoint mission and deliverable

Create an evidence-backed timeline for one lab-owned container through created → running → exec → attach/detach → stopped → restarted → exited → removed. Your deliverable is a directory of small text files that lets another engineer reconstruct what Docker object existed, which image content it used, what the primary process did, and what each lifecycle command changed.

Success criterion: do not merely show commands succeeded. For every transition, record before/after state and explain which identity remained stable and which process/state fields changed.

2. Preflight and assumptions

set -eu
EVIDENCE='ch05-evidence'
NAME='devops-academy-ch05-checkpoint'
LABEL='devops-academy.lab=ch05-checkpoint'
SOURCE='busybox:1.37.0'
mkdir -p "$EVIDENCE"

docker context show | tee "$EVIDENCE/context.txt"
docker version | tee "$EVIDENCE/docker-version.txt"
docker info --format 'OSType={{.OSType}} Arch={{.Architecture}} Driver={{.Driver}}' | tee "$EVIDENCE/docker-info.txt"

docker pull "$SOURCE" | tee "$EVIDENCE/pull.txt"
PINNED=$(docker image inspect "$SOURCE" --format '{{index .RepoDigests 0}}')
printf '%s\n' "$PINNED" | tee "$EVIDENCE/image-digest.txt"
test -n "$PINNED"

Record whether the daemon is native Linux, Docker Desktop, or Windows-container based, because host PID and signal observations are platform-sensitive. The checkpoint does not require Compose, Buildx, BuildKit, containerd, or runc commands, but the evidence packet may record their versions if available from previous chapters.

3. Guard exact resource ownership

if docker ps -a --filter "name=^/${NAME}$" --format '{{.ID}}' | grep -q .; then
  echo "STOP: $NAME already exists; do not delete it unless you created it for this checkpoint." >&2
  exit 1
fi

docker ps -a --filter "label=$LABEL" --format 'ID={{.ID}} Name={{.Names}} Status={{.Status}}' | tee "$EVIDENCE/preexisting-labels.txt"

A clean lab should show no matching container. If something appears, choose another checkpoint label/name rather than assuming ownership.

4. Write predictions before creating anything

Create ch05-evidence/predictions.txt with at least these statements in your own words:

  • After docker create, the container object will exist but .State.Running will be false and there will be no active primary PID.
  • After docker start, the container ID will remain the same while a primary PID becomes active.
  • After a bounded docker exec finishes, the primary PID and container ID should remain stable.
  • After graceful stop, the object will remain inspectable with an exit code and finish timestamp.
  • After restart/start, the same container ID will have a new primary-process instance.
  • After docker rm, inspecting that container ID/name should fail because the Docker object was removed.

5. Transition 1 — create the object, do not start it

docker create -it \
  --name "$NAME" \
  --label "$LABEL" \
  --stop-timeout 4 \
  "$PINNED" \
  sh -c 'trap "echo checkpoint-term; exit 0" TERM; echo checkpoint-start; while :; do sleep 1; done' \
  | tee "$EVIDENCE/create-id.txt"

CID=$(docker inspect "$NAME" --format '{{.Id}}')
printf '%s\n' "$CID" | tee "$EVIDENCE/container-id.txt"

docker inspect "$NAME" --format \
'ID={{.Id}} Name={{.Name}} Image={{.Image}} Status={{.State.Status}} Running={{.State.Running}} Pid={{.State.Pid}} Exit={{.State.ExitCode}} RestartCount={{.RestartCount}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}} Command={{.Path}} {{json .Args}} Labels={{json .Config.Labels}} Mounts={{json .Mounts}} Networks={{json .NetworkSettings.Networks}}' \
  | tee "$EVIDENCE/01-created.txt"

Verify the first prediction now. If the object is already running, stop: you did not execute the intended lifecycle path.

6. Transition 2 — start and capture primary-process evidence

docker start "$NAME" | tee "$EVIDENCE/start.txt"
sleep 1
PID1=$(docker inspect "$NAME" --format '{{.State.Pid}}')
printf '%s\n' "$PID1" | tee "$EVIDENCE/pid-first.txt"

docker inspect "$NAME" --format \
'ID={{.Id}} Status={{.State.Status}} Running={{.State.Running}} Pid={{.State.Pid}} Exit={{.State.ExitCode}} RestartCount={{.RestartCount}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}}' \
  | tee "$EVIDENCE/02-running.txt"

docker logs --timestamps "$NAME" | tee "$EVIDENCE/logs-after-start.txt"

Confirm that CID did not change and that a process PID now exists. If you are on Docker Desktop, treat that PID as daemon/VM-side runtime evidence rather than a direct Windows/macOS host process identity.

7. Transition 3 — run and finish a bounded exec process

docker exec "$NAME" sh -c 'echo checkpoint-exec; id; ps' \
  | tee "$EVIDENCE/exec.txt"

docker inspect "$NAME" --format 'ID={{.Id}} Status={{.State.Status}} Pid={{.State.Pid}}' \
  | tee "$EVIDENCE/03-after-exec.txt"

Explain in your review why the exec command ending did not end the container. The primary process remained the lifecycle owner.

8. Transition 4 — attach and detach without terminating PID 1

In a second terminal, run docker attach devops-academy-ch05-checkpoint. Detach with Ctrl-p, Ctrl-q. Do not press Ctrl-c. Back in the evidence terminal:

docker inspect "$NAME" --format 'ID={{.Id}} Status={{.State.Status}} Pid={{.State.Pid}}' \
  | tee "$EVIDENCE/04-after-attach.txt"

docker logs --timestamps "$NAME" | tee "$EVIDENCE/logs-after-attach.txt"

If you cannot perform an interactive attach safely, write attach not observed — terminal limitation in the evidence directory and explain the expected stream relationship. Do not fabricate an observation.

9. Transition 5 — stop gracefully and preserve exit state

docker stop --timeout 4 "$NAME" | tee "$EVIDENCE/stop.txt"

docker inspect "$NAME" --format \
'ID={{.Id}} Status={{.State.Status}} Running={{.State.Running}} Pid={{.State.Pid}} Exit={{.State.ExitCode}} RestartCount={{.RestartCount}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}}' \
  | tee "$EVIDENCE/05-stopped.txt"

docker logs --timestamps "$NAME" | tee "$EVIDENCE/logs-after-stop.txt"

Look for the checkpoint-term marker. Record the actual exit code. Do not alter it to match prose expectations.

10. Transition 6 — restart/start the same object and compare PIDs

docker start "$NAME" | tee "$EVIDENCE/start-again.txt"
sleep 1
PID2=$(docker inspect "$NAME" --format '{{.State.Pid}}')
printf '%s\n' "$PID2" | tee "$EVIDENCE/pid-second.txt"

docker inspect "$NAME" --format \
'ID={{.Id}} Status={{.State.Status}} Pid={{.State.Pid}} RestartCount={{.RestartCount}} Started={{.State.StartedAt}}' \
  | tee "$EVIDENCE/06-restarted.txt"

printf 'Container ID=%s\nFirst PID=%s\nSecond PID=%s\n' "$CID" "$PID1" "$PID2" \
  | tee "$EVIDENCE/identity-comparison.txt"

This step deliberately uses docker start after a stopped object so the semantics are obvious. You may repeat with docker restart --timeout 4 only after preserving evidence and explaining that restart combines stop/start on the same object.

11. Transition 7 — create an intentional clean exit while retaining evidence

To place the same object into an exited state with a deterministic command would require changing its configured command, which Docker does not do through start. Instead, stop it gracefully again and treat that stopped/exited state as the retained post-process object:

docker stop --timeout 4 "$NAME" | tee "$EVIDENCE/final-stop.txt"
docker inspect "$NAME" --format \
'ID={{.Id}} Status={{.State.Status}} Running={{.State.Running}} Pid={{.State.Pid}} Exit={{.State.ExitCode}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}}' \
  | tee "$EVIDENCE/07-final-exited.txt"

The important concept is that exited is a process state while the container object is still present. Preserve this packet before deletion.

12. Optional event timeline

If you recorded events in another terminal during the checkpoint, save only the bounded lab-relevant window. A practical method is:

docker events \
  --since '30m' \
  --until '0s' \
  --filter "container=$CID" \
  | tee "$EVIDENCE/events.txt"

Event retention/format can vary with daemon lifetime and timing. If the bounded query returns nothing, record that limitation; do not manufacture an event list.

13. Review the evidence before cleanup

Your packet should let you answer all of these:

  • Which exact image digest was used?
  • What was the full container ID and human-readable name?
  • What state existed immediately after create, start, exec, attach, stop, and second start?
  • Did the primary PID change when the process was recreated?
  • What stop timeout and command were configured?
  • What exit code/timestamps were retained?
  • Were there any mounts or non-default network attachments?
  • Which observations are platform-specific or not observed?

14. Transition 8 — exact removal and proof of cleanup

docker inspect "$NAME" --format 'ID={{.Id}} Name={{.Name}} Labels={{json .Config.Labels}} Status={{.State.Status}}' \
  | tee "$EVIDENCE/pre-remove.txt"

docker rm "$NAME" | tee "$EVIDENCE/remove.txt"

if docker inspect "$NAME" >"$EVIDENCE/post-remove.txt" 2>&1; then
  echo 'Unexpected: container still exists' >&2
  exit 1
else
  echo 'Expected: container object no longer resolves' | tee -a "$EVIDENCE/post-remove.txt"
fi

docker ps -a --filter "label=$LABEL" --format 'ID={{.ID}} Name={{.Names}} Status={{.Status}}' \
  | tee "$EVIDENCE/post-cleanup-label-query.txt"

Do not remove the source image, networks, volumes, or other containers. The checkpoint owns only the exact object it created.

15. Operational review and Chapter 06 handoff

Chapter 05 added a precise operational rule to the Docker model: container object state and primary-process state must be observed separately. You can now distinguish create/start/run, exec/attach, graceful stop/hard kill, explicit restart/restart policy, and object removal without collapsing them into “Docker restarted the app.”

Chapter 06 deepens the process/filesystem side of that model: writable layers, docker cp, exec workflows, process trees, signals, and PID 1. Carry forward the evidence discipline from this checkpoint—especially the difference between a Docker object, PID 1, an exec process, and runtime filesystem mutation.

Next chapter

Next: Container Filesystems, Processes, Signals, and PID 1

Move from lifecycle state to the filesystem/process mechanics inside a running container.

Knowledge check

Which identity should remain stable across the stop/start sequence: full container ID or primary PID?

Why does the checkpoint save 07-final-exited.txt before docker rm?

If the attach step cannot be observed, what is the correct evidence response?

What would docker rm -f add to this checkpoint's normal cleanup?

Does a successfully restarted container prove the external application is healthy?

Official references and version notes

  • docker container command group — current container-management surface, including create, run, start, stop, restart, kill, rm, exec, attach, inspect, pause, and wait.
  • docker container create — creates a container object without starting its primary process and records runtime configuration such as restart policy and stop timeout.
  • docker container run — create-and-start convenience behavior, foreground/detached operation, automatic removal, signal proxying, and stop configuration.
  • docker container start — starts an existing stopped container and optionally attaches standard streams.
  • docker container stop — graceful stop signal, timeout, and eventual SIGKILL escalation semantics.
  • docker container kill — immediate/default SIGKILL behavior and explicit signal selection.
  • docker container restart — stop-then-start semantics, configurable signal, and timeout behavior.
  • docker container rm — exact container removal; force-removing a running container uses SIGKILL and -v affects anonymous volumes.
  • docker container exec — starts an additional command only while the container's primary PID 1 is running; exec commands are not automatically restarted with the container.
  • docker container attach — attaches local standard streams to the existing ENTRYPOINT/CMD process, signal-proxy implications, detach keys, and throughput caveats.
  • docker container inspect — low-level container configuration and state evidence, including PID, exit code, restart count, timestamps, mounts, and networks.
  • Start containers automatically — restart-policy behavior, successful-start monitoring, manual-stop interaction, and distinction from live restore.
  • Docker Engine 29 release notes — current Engine 29 baseline; Engine 29.7 added the daemon-level default-stop-timeout option and 29.8.1 is the current patch release at chapter verification time.
Current baseline, not a frozen requirement

Verified 2026-09-21: Docker Engine 29.8.1 is the current Engine 29 patch release. Docker's current CLI documentation defines docker exec as a command that exists only while the container's primary process is running; docker stop sends the configured stop signal and escalates to SIGKILL after the timeout; docker kill defaults to SIGKILL; and docker rm --force kills a running container before removing the object. Linux and Windows defaults and process-isolation behavior differ, so every executable lab records the actual client/server, OS type, architecture, stop configuration, image digest, and container state observed on the learner's environment.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.