Chapter 34Lesson 05~215 minutes

Checkpoint Lab — Docker Engine API, SDKs, Remote Daemons, TLS Authentication, Contexts, and Automation Clients

Build an idempotent automation client that targets one named disposable context, ensures exactly one labeled container is running, verifies it by immutable ID, records evidence, and cleans only that identity.

CheckpointPython clientNamed contextEvidence packetChapter 35 bridge

Learning objectives

  • Create a named disposable local context and capture its endpoint before any mutation.
  • Run a small idempotent Python automation client that uses explicit Docker CLI context selection and controller-owned labels.
  • Predict and independently verify context, object, process, and cleanup state transitions.
  • Bind all verification and cleanup to the exact container ID returned or discovered by the client.
  • Produce an evidence packet that separates connection identity, API compatibility, object state, workload state, and cleanup outcome.

1. Checkpoint scenario and safety model

You will create dca34-checkpoint, a named context that points to your existing local Engine. A Python standard-library client will invoke Docker CLI commands with --context dca34-checkpoint. It ensures exactly one container carrying the lab labels reaches running state, proves the exact ID, and later removes only that ID.

This design deliberately uses the Docker CLI rather than importing a third-party SDK so the checkpoint remains free/local and works anywhere the Docker CLI/context feature is available. Earlier lessons already exercised raw REST and the optional Python SDK.

Scope: use only an authorized local disposable Docker environment. The checkpoint never changes daemon listeners, TLS settings, or firewall policy.

2. Preflight and tool assumptions

set -eu
LAB=dca34-checkpoint
EVIDENCE="$LAB/evidence"
mkdir -p "$EVIDENCE"

date -u +%Y-%m-%dT%H:%M:%SZ | tee "$EVIDENCE/time-before.txt"
docker context show | tee "$EVIDENCE/context-before.txt"
docker context inspect "$(docker context show)" > "$EVIDENCE/context-before.json"
docker version | tee "$EVIDENCE/docker-version.txt"
docker compose version 2>&1 | tee "$EVIDENCE/compose-version.txt" || true
docker buildx version 2>&1 | tee "$EVIDENCE/buildx-version.txt" || true
docker buildx inspect 2>&1 | tee "$EVIDENCE/buildkit-worker.txt" || true
containerd --version 2>&1 | tee "$EVIDENCE/containerd-version.txt" || true
runc --version 2>&1 | tee "$EVIDENCE/runc-version.txt" || true
python --version | tee "$EVIDENCE/python-version.txt"

Docker Desktop may not expose containerd or runc binaries on the client host. Record “not directly exposed” rather than inferring versions.

3. Pull and record the synthetic image identity

docker pull alpine:3.22
docker image inspect alpine:3.22 > "$EVIDENCE/image-inspect.json"
docker image inspect --format 'id={{.Id}} repoDigests={{json .RepoDigests}}' alpine:3.22   | tee "$EVIDENCE/image-identity.txt"

4. Create a named local context without changing the default

docker context rm -f dca34-checkpoint >/dev/null 2>&1 || true

docker context create   --description 'DevOps Academy Chapter 34 checkpoint'   --docker host=unix:///var/run/docker.sock   dca34-checkpoint

docker context inspect dca34-checkpoint   | tee "$EVIDENCE/context-checkpoint.json"

docker --context dca34-checkpoint version   | tee "$EVIDENCE/version-checkpoint.txt"

If your local Engine endpoint is not /var/run/docker.sock, substitute the endpoint from your existing local context. Do not create a TCP listener just to satisfy the checkpoint.

5. Write predictions before automation

cat > "$EVIDENCE/predictions.md" <<'EOF'
# Predictions
1. The named context will resolve to the same authorized local Engine endpoint used for the lab.
2. First ensure-run will create exactly one container with both controller labels and record its exact ID.
3. Second ensure-run will reuse the same ID rather than create a duplicate.
4. The verified container will be running and use the recorded Alpine image identity.
5. Cleanup will remove only the recorded ID; a label-filtered post-check will return no checkpoint containers.
EOF
cat "$EVIDENCE/predictions.md"

6. Create the idempotent automation client

from __future__ import annotations
import argparse
import json
import subprocess
import sys

CTX = "dca34-checkpoint"
NAME = "dca34-managed"
LABELS = {
    "devops-academy.lab": "chapter34-checkpoint",
    "devops-academy.controller": "dca34-client",
}
IMAGE = "alpine:3.22"


def docker(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
    cmd = ["docker", "--context", CTX, *args]
    return subprocess.run(cmd, text=True, capture_output=True, check=check)


def owned_ids() -> list[str]:
    result = docker(
        "ps", "-aq",
        "--filter", f"label=devops-academy.lab={LABELS['devops-academy.lab']}",
        "--filter", f"label=devops-academy.controller={LABELS['devops-academy.controller']}",
    )
    return [line.strip() for line in result.stdout.splitlines() if line.strip()]


def inspect(cid: str) -> dict:
    result = docker("inspect", cid)
    data = json.loads(result.stdout)
    if len(data) != 1:
        raise RuntimeError(f"expected one inspect object for {cid}")
    return data[0]


def assert_owned(obj: dict) -> None:
    labels = obj.get("Config", {}).get("Labels") or {}
    for key, value in LABELS.items():
        if labels.get(key) != value:
            raise RuntimeError(f"refusing object without expected {key}={value}")


def ensure_running() -> str:
    ids = owned_ids()
    if len(ids) > 1:
        raise RuntimeError(f"refusing ambiguous ownership: {ids}")

    if not ids:
        create = docker(
            "create", "--name", NAME,
            "--label", f"devops-academy.lab={LABELS['devops-academy.lab']}",
            "--label", f"devops-academy.controller={LABELS['devops-academy.controller']}",
            IMAGE,
            "sh", "-c", "trap 'exit 0' TERM INT; while :; do sleep 2; done",
        )
        cid = create.stdout.strip()
    else:
        cid = ids[0]

    obj = inspect(cid)
    assert_owned(obj)
    if obj["Name"].lstrip("/") != NAME:
        raise RuntimeError(f"owned ID has unexpected name: {obj['Name']}")

    if not obj.get("State", {}).get("Running", False):
        docker("start", cid)

    obj = inspect(cid)
    assert_owned(obj)
    if not obj.get("State", {}).get("Running", False):
        raise RuntimeError("container failed to reach running state")

    print(json.dumps({
        "id": obj["Id"],
        "name": obj["Name"].lstrip("/"),
        "status": obj["State"]["Status"],
        "pid": obj["State"]["Pid"],
        "image_id": obj["Image"],
        "labels": obj["Config"].get("Labels") or {},
    }, sort_keys=True))
    return obj["Id"]


def cleanup(expected_id: str) -> None:
    ids = owned_ids()
    if expected_id not in ids:
        raise RuntimeError(f"expected ID {expected_id} is not owned/present: {ids}")
    obj = inspect(expected_id)
    assert_owned(obj)
    docker("rm", "-f", expected_id)
    remaining = owned_ids()
    if remaining:
        raise RuntimeError(f"owned containers remain: {remaining}")
    print(json.dumps({"removed": expected_id}))


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("action", choices=["ensure", "cleanup"])
    parser.add_argument("--id")
    args = parser.parse_args()
    if args.action == "ensure":
        ensure_running()
        return 0
    if not args.id:
        parser.error("cleanup requires --id")
    cleanup(args.id)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Save this as dca34-checkpoint/client.py. Every Docker invocation contains the named context, and the client refuses ambiguous ownership rather than guessing which object is “latest.”

7. First ensure-run: create and capture exact identity

python "$LAB/client.py" ensure   | tee "$EVIDENCE/ensure-1.json"

CID="$(python -c 'import json; print(json.load(open("dca34-checkpoint/evidence/ensure-1.json"))["id"])')"
printf '%s
' "$CID" | tee "$EVIDENCE/container-id.txt"

docker --context dca34-checkpoint inspect "$CID"   > "$EVIDENCE/container-inspect-1.json"

8. Second ensure-run: prove idempotency

python "$LAB/client.py" ensure   | tee "$EVIDENCE/ensure-2.json"

FIRST_ID="$(python -c 'import json; print(json.load(open("dca34-checkpoint/evidence/ensure-1.json"))["id"])')"
SECOND_ID="$(python -c 'import json; print(json.load(open("dca34-checkpoint/evidence/ensure-2.json"))["id"])')"
test "$FIRST_ID" = "$SECOND_ID"

docker --context dca34-checkpoint ps -a   --filter label=devops-academy.controller=dca34-client   --format '{{.ID}} {{.Names}} {{.Status}}'   | tee "$EVIDENCE/owned-after-second-run.txt"

One ID across both runs proves convergence at the Docker-object layer. It does not yet prove image identity or runtime behavior; those are verified next.

9. Verify image, process, API, logs, and events independently

CID="$(cat "$EVIDENCE/container-id.txt")"

docker \
  --context dca34-checkpoint inspect \
  --format 'id={{.Id}} status={{.State.Status}} pid={{.State.Pid}} image={{.Image}} restart={{.RestartCount}}'   "$CID" | tee "$EVIDENCE/runtime-verification.txt"

docker \
  --context dca34-checkpoint version \
  --format 'client-api={{.Client.APIVersion}} server-api={{.Server.APIVersion}}'   | tee "$EVIDENCE/api-verification.txt"

docker --context dca34-checkpoint logs --timestamps "$CID"   | tee "$EVIDENCE/logs.txt" || true

docker --context dca34-checkpoint events   --since 10m --until 0s --filter container="$CID"   | tee "$EVIDENCE/events.txt" || true

10. Clean only the exact identity

CID="$(cat "$EVIDENCE/container-id.txt")"
python "$LAB/client.py" cleanup --id "$CID"   | tee "$EVIDENCE/cleanup.json"

docker --context dca34-checkpoint ps -a   --filter label=devops-academy.controller=dca34-client   --format '{{.ID}} {{.Names}} {{.Status}}'   | tee "$EVIDENCE/post-cleanup.txt"

An empty post-cleanup listing is expected. No unrelated container, image, network, volume, or build cache is removed.

11. Remove only client metadata created by the checkpoint

docker context rm dca34-checkpoint

date -u +%Y-%m-%dT%H:%M:%SZ | tee "$EVIDENCE/time-final.txt"
docker context show | tee "$EVIDENCE/context-final.txt"

Removing a context removes client-side context metadata; it does not delete daemon resources. Because the exact lab container was already removed by ID, rollback is complete.

12. Required evidence packet

Evidence family Required files / facts
Host/client UTC timestamps, Python, Docker CLI, Compose, Buildx/BuildKit, containerd/runc availability
Context original context; checkpoint context JSON; resolved endpoint URI
API compatibility client/server/API versions; statement that no fixed API override was used
Image human-readable Alpine tag plus image ID/RepoDigest evidence
Automation source client.py and controller label contract
First ensure JSON with exact ID/name/status/PID/image ID/labels
Idempotency second ensure JSON and equality proof for IDs
Runtime inspect state, PID, restart count, logs/events as applicable
Cleanup exact ID passed to cleanup; empty controller-label query afterward
Security no remote listener change, no socket mount, no private keys, no broad prune
Limitations running state is the target for this synthetic process; no application protocol/healthcheck exists

13. Interpret the checkpoint

The checkpoint demonstrates four independent boundaries: client target (named context and endpoint), API compatibility (negotiated client/server versions), object identity (exact ID plus controller labels), and runtime outcome (running process). A fifth boundary—application health—would need its own probe for a real service.

14. What Chapter 34 adds to the operating model

You can now automate Docker without treating the daemon as an anonymous local service. A production-quality control path identifies the daemon explicitly, authenticates transport, negotiates a supported API, scopes object ownership, captures immutable IDs, reconciles before retry, separates object success from workload health, and cleans only identities it owns.

Chapter 35

Docker Socket Security, Docker-in-Docker, Socket Mounting, Build Services, and CI Isolation Tradeoffs

Chapter 35 deepens the control-plane boundary: what really happens when CI jobs mount the Docker socket, run Docker-in-Docker, share a build daemon, or cross host privilege boundaries—and how to choose safer isolation patterns.

Knowledge check

What proves the checkpoint targets the intended daemon?

Why does the client refuse when two controller-labeled containers exist?

What proves the second ensure-run is idempotent?

Why does cleanup require the recorded ID instead of just the name?

What important production check is intentionally absent from this synthetic lab?

Official references and version notes

Checkpoint baseline: 2026-09-22. It uses only local Docker context/CLI capabilities and Python standard library. The image tag is recorded to a local image ID and RepoDigest before automation evidence is interpreted.

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.