Chapter 05Lesson 02~120 minutes

Running Containers: create, run, start, stop, restart, rm, exec, attach, inspect, and Lifecycle State: Guided Hands-On Workflow and Core Operations

This lesson exercises one disposable container through creation, start, inspection, bounded exec, safe attach/detach, graceful stop, explicit restart, foreground execution, exit-code capture, and exact removal. Every step records what changed in the object and what changed in the process.

Hands-oncreate/startexec/attachstop/restartExact cleanup

Learning objectives

  • Create a container without starting it, prove the created state, then start the same object and prove a new primary PID exists.
  • Run bounded exec commands and distinguish their lifetime from the primary process lifetime.
  • Attach and detach without accidentally terminating the container, then verify that the same container object and PID remain.
  • Compare graceful stop, explicit kill semantics, and restart while preserving state evidence.
  • Remove only the exact lab-owned container after recording exit code, timestamps, logs, labels, and image identity.
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. Lab scope and ownership

This guided lab uses one small Docker Official Image and only lab-owned container names. It intentionally separates create from start, then contrasts that workflow with run. The primary lab object is retained until the end so that you can inspect every transition.

Lab-owned names: devops-academy-ch05-main, devops-academy-ch05-fg, and devops-academy-ch05-exit, all labeled devops-academy.lab=ch05. Cleanup queries exact names/labels before removal. No broad prune is used.

2. Preflight: establish context, platform, and image identity

docker context show
docker version
docker info --format 'OSType={{.OSType}} Arch={{.Architecture}} Driver={{.Driver}}'

SOURCE='busybox:1.37.0'
docker pull "$SOURCE"
PINNED=$(docker image inspect "$SOURCE" --format '{{index .RepoDigests 0}}')
printf 'Pinned image: %s\n' "$PINNED"
test -n "$PINNED"

The tag is a convenient starting coordinate. The lab records the repository digest and uses it for the container objects so a later tag move cannot silently change the bytes. If the image already existed locally, that is acceptable; the evidence packet must say so.

3. Prove the lab names are not already owned

MAIN='devops-academy-ch05-main'
FG='devops-academy-ch05-fg'
EXIT='devops-academy-ch05-exit'
LABEL='devops-academy.lab=ch05'

for n in "$MAIN" "$FG" "$EXIT"; do
  docker ps -a --filter "name=^/${n}$" --format '{{.ID}} {{.Names}} {{.Status}}'
done

If any name already exists and you did not create it for this lab, stop and choose different names. Do not delete an object merely because its name matches an example.

4. Create without starting

The command below creates a TTY-enabled object whose primary shell traps TERM, prints lifecycle markers, and then loops. The TTY makes the later attach/detach demonstration safe and explicit.

docker create -it \
  --name "$MAIN" \
  --label "$LABEL" \
  --stop-timeout 4 \
  "$PINNED" \
  sh -c 'trap "echo TERM-seen; exit 0" TERM; echo primary-started; while :; do sleep 1; done'

docker inspect "$MAIN" --format \
'ID={{.Id}} Status={{.State.Status}} Running={{.State.Running}} Pid={{.State.Pid}} Exit={{.State.ExitCode}} Image={{.Image}}'

Expected evidence: the object exists, Status=created, Running=false, and there is no running primary PID. Configuration such as the command, label, and stop timeout already belongs to the object before execution.

5. Start the same object and prove process state

docker start "$MAIN"
docker logs "$MAIN"
docker inspect "$MAIN" --format \
'ID={{.Id}} Status={{.State.Status}} Running={{.State.Running}} Pid={{.State.Pid}} Started={{.State.StartedAt}} RestartCount={{.RestartCount}}'

Save the container ID and PID. The container ID should match the created object. The PID now identifies the current primary process instance. On Docker Desktop, remember that process isolation lives inside Docker's managed Linux VM rather than directly in the Windows/macOS host namespace.

6. Execute a bounded diagnostic command

docker exec "$MAIN" sh -c 'echo exec-process; id; ps'

docker inspect "$MAIN" --format \
'ID={{.Id}} Status={{.State.Status}} Pid={{.State.Pid}}'

The exec command creates an additional process in the running container. When it finishes, the primary loop remains. Do not edit application files here as a “fix”; the point is to prove that exec is a temporary interaction, not a durable image change.

7. Attach safely, then detach without stopping the container

Open a second terminal and run:

docker attach "$MAIN"

You are now attached to the existing primary process streams—not a new shell. Because the object was created with -it, detach using Ctrl-p, Ctrl-q. Do not press Ctrl-c for this exercise. Then verify from the first terminal:

docker inspect "$MAIN" --format 'Status={{.State.Status}} Running={{.State.Running}} Pid={{.State.Pid}}'
docker logs "$MAIN"

If your terminal/key bindings make interactive attach impractical, treat this step as an observed-architecture exercise and use docker logs; do not improvise signal shortcuts against a shared or important workload.

8. Stop gracefully and preserve the exit evidence

BEFORE_PID=$(docker inspect "$MAIN" --format '{{.State.Pid}}')
docker stop --timeout 4 "$MAIN"

docker inspect "$MAIN" --format \
'ID={{.Id}} Status={{.State.Status}} Running={{.State.Running}} Pid={{.State.Pid}} Exit={{.State.ExitCode}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}} RestartCount={{.RestartCount}}'
docker logs "$MAIN"
printf 'Previous primary PID: %s\n' "$BEFORE_PID"

You should see the TERM-seen marker and an exited object. The exact exit code depends on how the shell handles the trap and runtime; record what you observe rather than forcing an expected number into the evidence.

9. Restart the retained object and compare PID identity

docker restart --timeout 4 "$MAIN"
AFTER_PID=$(docker inspect "$MAIN" --format '{{.State.Pid}}')
CONTAINER_ID=$(docker inspect "$MAIN" --format '{{.Id}}')
printf 'Container ID: %s\nOld PID: %s\nNew PID: %s\n' "$CONTAINER_ID" "$BEFORE_PID" "$AFTER_PID"
docker inspect "$MAIN" --format 'Status={{.State.Status}} RestartCount={{.RestartCount}} Started={{.State.StartedAt}}'

This is the same Docker object with a newly started primary process. An explicit restart is not proof that an automatic restart policy is configured; inspect .HostConfig.RestartPolicy separately if that distinction matters.

10. Contrast with a foreground docker run

docker run --name "$FG" --label "$LABEL" "$PINNED" sh -c 'echo foreground-start; sleep 1; echo foreground-end'

docker inspect "$FG" --format \
'ID={{.Id}} Status={{.State.Status}} Exit={{.State.ExitCode}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}}'

Your terminal remains attached until the process exits. The object remains because this command did not use --rm. That retention is deliberate: you can now inspect the exit code and timestamps.

11. Capture a deliberate non-zero exit without losing the object

docker run --name "$EXIT" --label "$LABEL" "$PINNED" sh -c 'echo deliberate-failure >&2; exit 23' || true

docker inspect "$EXIT" --format \
'ID={{.Id}} Status={{.State.Status}} Exit={{.State.ExitCode}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}}'
docker logs "$EXIT"

Here || true is used only so the teaching shell can continue after the intentionally non-zero container command. The Docker evidence must still show the real exit code 23. In automation, do not hide an unexpected application failure this way.

12. Stop versus kill: understand the boundary without destructive theatrics

The lab already demonstrated graceful stop. Current Docker behavior makes the contrast clear: docker kill sends SIGKILL by default; docker stop sends the configured stop signal and waits before escalation. There is no educational value in killing the primary lab object just to prove SIGKILL exists, because that would bypass the graceful path you are learning to preserve.

If an incident requires a hard kill because a disposable process ignores termination, preserve logs/inspect/events first, state the reason, target the exact container ID, and verify the resulting exit state afterward.

13. Challenge: choose the correct lifecycle layer

Without copying the previous commands, answer these three scenarios: (1) you need to inspect a config file while PID 1 is healthy; (2) you need to watch the primary process output interactively without starting another process; (3) you need the object to exist for inspection before any process starts. Choose exec, attach/logs, or create for each and explain what state changes.

14. Guarded cleanup

docker ps -a --filter "label=$LABEL" --format 'ID={{.ID}} Name={{.Names}} Status={{.Status}}'

# Stop only the known running lab object.
docker stop --timeout 4 "$MAIN" 2>/dev/null || true

# Verify exact names again before deletion.
for n in "$MAIN" "$FG" "$EXIT"; do
  docker inspect "$n" --format 'ID={{.Id}} Name={{.Name}} Labels={{json .Config.Labels}}' 2>/dev/null || true
done

docker rm "$MAIN" "$FG" "$EXIT"

docker ps -a --filter "label=$LABEL"

The source image is intentionally retained because the lab cannot prove no other work uses it. No volume/network/image prune is part of this exercise.

Next lesson

Next: Configuration, Design Choices, and Tradeoffs

Use the observed lifecycle evidence to choose create/run, foreground/detached, exec/diagnostic-container, retention, and shutdown strategies deliberately.

Knowledge check

Why did the lab use docker create before docker start?

After the exec command exits, why does the container remain running?

Why did the foreground example omit --rm?

What proves that docker restart reused the same container object?

Why is a hard kill not used as routine cleanup in the lab?

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.