Chapter 01Lesson 02~105 minutes

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.

Hands-onContextImage digestInspectNamespaces

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.
Chapter 01 technical baseline — verified 2026-09-20. Docker Engine 29.8.1 is the current Engine 29 patch release in Docker's release notes. Its packages update the bundled containerd static binaries to v2.3.5. Docker and OCI internals are version-sensitive, so every lab starts by recording the actual client, server, API, kernel, context, cgroup, runtime, and image identity instead of assuming this baseline.

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.

Lab boundary. Use a personal/disposable Docker host or Docker Desktop environment you are authorized to administer. Do not run the namespace/PID host-inspection commands against an employer's shared production host. Do not change socket permissions, daemon TCP exposure, seccomp, LSM policy, or privilege settings to make a command work.

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 version shows 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?

Why pull by tag but run by the resolved RepoDigest?

On Docker Desktop, why might the PID from docker inspect not exist in the physical host process table?

What does an empty .Mounts array prove in this lab?

Why is docker rm -f $(docker ps -q) inappropriate cleanup for this lab?

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.

Next lesson

Next: Containers, Virtual Machines, Linux Isolation, OCI Standards, and Docker Architecture: Configuration, Design Choices, and Tradeoffs

Continue with the next lesson in the course sequence and carry forward the evidence-first Docker operating model.

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.

Current-version note

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.

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