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.
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.
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.
Knowledge check
What proves the checkpoint targets the intended daemon?
The named context plus inspected endpoint and version response, captured before mutations.
Why does the client refuse when two controller-labeled containers exist?
Ambiguous ownership means it cannot safely decide which object represents desired state; failing closed is safer than guessing.
What proves the second ensure-run is idempotent?
It returns the same exact container ID and the controller-label query still shows one owned object.
Why does cleanup require the recorded ID instead of just the name?
Names can be reused; the recorded ID binds cleanup to the exact object created/discovered by this checkpoint.
What important production check is intentionally absent from this synthetic lab?
Application-level health. The workload only needs to remain running, so there is no HTTP/TCP/business-function readiness claim.
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.
-
Docker Docs — Docker Engine API
— versioned REST API, current version matrix, negotiation rules,
and
DOCKER_API_VERSION. - Docker Docs — Develop with Docker Engine SDKs — supported SDK workflow, Go/Python clients, and version-selection guidance.
- Docker Docs — SDK and API examples — equivalent CLI, Go, Python, and raw HTTP operations.
- Docker Docs — Docker contexts — endpoint identity, TLS metadata, selection, and inspection.
-
Docker CLI reference
—
--context,--host,DOCKER_CONTEXT,DOCKER_HOST, and TLS client options. - Docker Docs — Protect the Docker daemon socket — SSH and mutual-TLS patterns and private-key authority warnings.
- Docker Docs — Configure remote access — remote endpoint risks, TCP configuration, and firewall considerations.
- Docker Docs — Docker Engine security — host-control implications of daemon access and secure remote transport.
-
Docker Docs —
docker version— negotiated API reporting andDOCKER_API_VERSIONbehavior. - Docker Engine 29 release notes — Engine 29 behavior and compatibility baseline.
- PyPI — Docker SDK for Python — current Python package release identity used by the optional SDK lab.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.