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.
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
execcommands 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, explicitkillsemantics, andrestartwhile preserving state evidence. - Remove only the exact lab-owned container after recording exit code, timestamps, logs, labels, and image identity.
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.
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.
Knowledge check
Why did the lab use docker create before
docker start?
It makes the object/process boundary observable. You can inspect
the complete container object in created state
before any primary process exists.
After the exec command exits, why does the container remain running?
The exec process is additional. The container lifecycle is still owned by the primary process, which is continuing its loop.
Why did the foreground example omit --rm?
Retaining the object preserves inspectable exit code and timestamps. Automatic removal would trade diagnostic evidence for cleanup convenience.
What proves that docker restart reused the same
container object?
The container ID remains the same across the restart while the primary process PID can change.
Why is a hard kill not used as routine cleanup in the lab?
It bypasses graceful application shutdown and can lose useful evidence or state. Exact graceful stop plus verified removal is the safer default.
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
-vaffects 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-timeoutoption and 29.8.1 is the current patch release at chapter verification time.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.