Containers, Virtual Machines, Linux Isolation, OCI Standards, and Docker Architecture: Guided Hands-On Workflow and Core Operations
Turn the Chapter 01 architecture into one safe, reproducible workflow: identify the Docker endpoint, resolve an image to immutable content, run a bounded container, correlate Docker object state with process/namespace/cgroup evidence, and clean up only the resource you own.
Learning objectives
- Perform a read-only Docker environment preflight before creating any resource.
- Resolve a mutable image tag to a repository digest and use the digest as the runtime reference.
- Correlate Docker container metadata with the container process view and, on native Linux, the host process view.
- Explain which component owns client/context, Docker object, runtime process, kernel isolation, and log evidence.
- Apply exact name/label cleanup guards instead of broad prune or latest-object shortcuts.
1. The workflow: inspect first, then create one bounded container
This lesson turns the Chapter 01 architecture into a repeatable workflow. You will not modify daemon configuration, publish ports, mount host directories, or grant extra capabilities. The lab creates one labeled container from a digest-resolved Alpine image, inspects its state from multiple layers, and removes only that container.
2. Preflight — prove which Docker you are about to use
A Docker CLI can target a local Unix socket, Docker Desktop's VM, an SSH context, or a TLS-protected remote daemon. Before creating a resource, prove the selected endpoint.
set -eu
LAB_CONTAINER="da-ch01-workflow"
LAB_LABEL="devops-academy.lab=chapter01"
LAB_IMAGE_TAG="alpine:3.22"
printf 'UTC: '; date -u +%Y-%m-%dT%H:%M:%SZ
printf 'Context: '; docker context show
docker context inspect "$(docker context show)"
docker version
docker info
Record whether the server describes native Linux Engine, Docker Desktop, rootless mode, its storage driver/image store, cgroup version/driver, default runtime, and security options. If the context points at a remote daemon you did not intend to use, stop here. The safest mistake is the one caught before resource creation.
3. Capture component/tool evidence without assuming every binary is on PATH
docker version is authoritative for client/server/API
identity. docker info reports server capabilities and
runtime information. On a native Linux package installation,
containerd --version and
runc --version may also be available. Docker Desktop
may keep those binaries inside its VM, so absence from the
physical-host PATH is not evidence that the daemon does not use
them.
docker version
docker info
docker compose version 2>/dev/null || echo "Compose plugin not present or not on this client"
docker buildx version 2>/dev/null || echo "Buildx plugin not present or not on this client"
containerd --version 2>/dev/null || echo "containerd binary not exposed on this host PATH"
runc --version 2>/dev/null || echo "runc binary not exposed on this host PATH"
Engine 29.8.1 release notes package containerd static binaries v2.3.5, but your environment might be a different Engine patch, distribution package, Desktop bundle, or managed installation. Preserve actual output rather than rewriting it to match this lesson.
4. Resolve mutable discovery input to immutable image evidence
Use a tag only to find current content. After pulling, capture the exact repository digest and use that digest for the container. This avoids pretending that a mutable tag is an immutable release identity.
docker pull "$LAB_IMAGE_TAG"
ALPINE_REF="$(docker image inspect "$LAB_IMAGE_TAG" --format '{{index .RepoDigests 0}}')"
IMAGE_ID="$(docker image inspect "$LAB_IMAGE_TAG" --format '{{.Id}}')"
printf 'RepoDigest: %s
Image config ID: %s
' "$ALPINE_REF" "$IMAGE_ID"
The repository digest and local image/config identity solve different questions. Chapter 04 explains indexes, manifests, configs, and layers in depth. For now, preserve both strings as evidence.
5. Predict the state transition before you run it
| Prediction | Expected evidence |
|---|---|
| A new Docker container object will exist. |
A unique container ID, name da-ch01-workflow,
and lab label appear in docker inspect.
|
| The main process will run for about 15 minutes unless removed. |
.State.Running=true, a nonzero host PID, and
sleep visible through docker top.
|
| The process gets a container network endpoint. |
.NetworkSettings.Networks identifies the
attached network and endpoint information.
|
| No persistent host data mount will be created. |
.Mounts should be empty for this command.
|
6. Create and run the exact digest
docker run -d \
--name "$LAB_CONTAINER" \
--label "$LAB_LABEL" \
"$ALPINE_REF" \
sh -c 'echo "chapter01 workflow started"; exec sleep 900'
docker ps --filter "name=^/${LAB_CONTAINER}$" --no-trunc
docker logs "$LAB_CONTAINER"
docker run is a convenience operation that creates a
container object and starts its main process. Later chapters
separate create from start explicitly. The
label gives cleanup another identity guard in addition to the name.
7. Read the Docker object before reading the host
docker inspect "$LAB_CONTAINER" --format \
'id={{.Id}} name={{.Name}} image-config={{.Image}} status={{.State.Status}} pid={{.State.Pid}}'
docker inspect "$LAB_CONTAINER" --format 'labels={{json .Config.Labels}}'
docker inspect "$LAB_CONTAINER" --format 'networks={{json .NetworkSettings.Networks}}'
docker inspect "$LAB_CONTAINER" --format 'mounts={{json .Mounts}}'
docker top "$LAB_CONTAINER"
At this layer, Docker owns the container object's desired/runtime configuration and reports the PID assigned by the daemon host. This still does not prove what the process sees inside its namespaces.
8. Compare the container's view with the daemon-host view
Inside the container, PID 1 is the process namespace's root. The kernel release is a useful sanity check: on native Linux Engine it should match the host kernel. On Desktop, it belongs to the Desktop VM.
docker exec "$LAB_CONTAINER" sh -c '
echo "kernel: $(uname -srmo)"
echo "hostname: $(hostname)"
echo "pid namespace process view:"
ps -o pid,ppid,user,comm
echo "namespace links:"
for n in pid mnt net uts ipc; do printf "%s -> " "$n"; readlink "/proc/1/ns/$n"; done
echo "cgroup membership:"
cat /proc/1/cgroup
'
Because the command runs inside the container's namespace view,
/proc/1 refers to its PID 1. That output is evidence
about the container process perspective, not the physical host's
process tree.
9. Native Linux only — map the same process from the host side
On a native Linux Engine host you control, retrieve the daemon-host PID and inspect its namespaces/cgroup. This is read-only. Docker Desktop users should skip this section because the daemon-host PID lives inside the Desktop VM.
PID="$(docker inspect "$LAB_CONTAINER" --format '{{.State.Pid}}')"
printf 'daemon-host PID: %s
' "$PID"
ps -fp "$PID"
for n in pid mnt net uts ipc; do
printf '%s -> ' "$n"
readlink "/proc/$PID/ns/$n"
done
printf 'cgroup membership:
'
cat "/proc/$PID/cgroup"
printf 'host namespace examples:
'
readlink /proc/1/ns/pid
readlink /proc/1/ns/net
The namespace inode identifiers should make the abstraction concrete: the container process is on the same kernel but not necessarily in the same PID, mount, network, UTS, or IPC namespaces as host PID 1. Depending on daemon mode and platform, some namespace relationships can differ; inspect instead of assuming.
10. Map each observation to the component that owns it
| Observation/action | Primary owner | Why that matters |
|---|---|---|
| Select context | Docker CLI/client configuration | A wrong context can target the wrong daemon before any container exists. |
| Container configuration/metadata | Docker Engine/dockerd |
Engine API is the supported Docker control surface. |
| Runtime process creation | containerd/runtime path and OCI runtime | Explains why runtime failures differ from image lookup or client failures. |
| PID/network/mount namespace behavior | Linux kernel configured by runtime | Isolation evidence is kernel state, not merely a Docker UI label. |
| Application stdout | Container process; collected through Docker logging path | Logs prove emitted output, not health or reachability. |
Current Docker documentation also describes an experimental embedded-containerd mode. That is why production diagnostics should avoid depending on undocumented internal sockets or treating Docker's managed containerd state as a general-purpose containerd installation.
11. Challenge — choose the failing layer before changing anything
For each symptom, name the first layer you would inspect:
-
docker versionshows client information but cannot contact a server. - The daemon is reachable, but pulling the image returns a registry authentication error.
- The image is present, but the container exits immediately with code 127.
- The process is running, but the expected host file is absent inside the container.
- The process is running and a network exists, but no host port was published.
A good answer starts respectively with context/daemon connectivity, registry identity/access, container command/runtime evidence, mount configuration, and network/publication configuration. The goal is causal localization, not memorizing a rescue command.
12. Verify ownership, then clean exactly one resource
docker inspect "$LAB_CONTAINER" --format \
'name={{.Name}} lab={{index .Config.Labels "devops-academy.lab"}} status={{.State.Status}}'
# Expected: the exact lab name and lab=chapter01.
docker rm -f "$LAB_CONTAINER"
docker ps -a --filter "name=^/${LAB_CONTAINER}$"
# No rows means the container object is gone.
Do not run a broad prune. The pulled image is safe to leave cached. If this is a fully disposable host and you want to remove it later, target the exact digest after confirming no other lab depends on it.
Knowledge check
Why does the lab inspect the active Docker context before creating the container?
Because the CLI can target different local or remote daemons. Context identity is part of the side-effect boundary; a correct command against the wrong daemon is still an operational failure.
Why pull by tag but run by the resolved
RepoDigest?
The tag is a convenient discovery reference, while the digest records the exact registry content selected for the lab. Running the resolved digest reduces ambiguity about which image bytes were intended.
On Docker Desktop, why might the PID from
docker inspect not exist in the physical host
process table?
Because the Docker daemon and Linux containers run inside Docker Desktop's managed Linux VM. The reported PID belongs to that daemon host, not necessarily the physical macOS/Windows/Linux host namespace.
What does an empty .Mounts array prove in this
lab?
It proves Docker did not attach explicit volumes or bind/tmpfs mounts to this container. It does not mean the container has no filesystem; the image plus writable layer still provide its root filesystem.
Why is docker rm -f $(docker ps -q) inappropriate
cleanup for this lab?
It has ambiguous broad scope and can destroy unrelated workloads. Cleanup must target the exact lab-owned container after verifying its name/label identity.
Summary
You have now traced one digest-resolved image through the active Docker context, Engine API/daemon, container object, runtime process, namespace/cgroup evidence, network attachment, logs, and exact cleanup. The workflow is intentionally small: inspect before mutation, predict state changes, preserve IDs/digests, and map each observation to the layer that owns it.
Official references and version notes
Version-sensitive statements in this chapter were rechecked against primary documentation on 2026-09-20. Docker Engine, Docker Desktop, containerd, runc, OCI specifications, security defaults, and platform integration continue to evolve. Record the versions actually reported by your own environment before treating an example as production policy.
- Docker documentation — product documentation and current operating guidance.
- Docker Engine — Engine architecture, daemon, objects, and administration.
- Docker Engine 29 release notes — current 29.x changes; 29.8.1 was released 2026-09-15.
- Docker Engine API — API negotiation and current client/server compatibility guidance.
- Docker Engine security — namespaces, cgroups, daemon attack surface, and kernel hardening.
- Seccomp security profiles — default syscall filtering and why disabling it is not a routine fix.
- Runtime metrics — current cgroup v2 behavior and container cgroup inspection.
- Rootless mode — daemon/container privilege reduction and user-namespace model.
-
Docker Desktop on Linux
— the VM-backed Desktop execution boundary and
desktop-linuxcontext. - Open Container Initiative — Image, Runtime, and Distribution specifications.
- OCI Runtime Specification — runtime configuration, execution environment, and lifecycle; OCI Runtime Spec v1.3.0 was released in November 2025.
Docker Engine 29.8.1 is the current Engine 29 patch release as of
this lesson's verification date. The current Engine API page
contains an example for 29.8.1 reporting API 1.56 while its
version matrix lists Docker 29.8 with maximum API 1.55. This
chapter therefore treats the output of
docker version on the actual client/daemon pair as
authoritative lab evidence and does not hard-code an expected
negotiated API number.
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.