Chapter 35Lesson 02~210 minutes

Docker Socket Security, Docker-in-Docker, Socket Mounting, Build Services, and CI Isolation Tradeoffs: Guided Hands-On Workflow and Core Operations

Inspect Docker authority without exploiting it, then use two disposable Buildx builder instances to model per-job build state while keeping untrusted-job socket and privileged patterns out of the runnable path.

Read-only inspectionBuildxPer-job buildersEvidenceSafe lab

Learning objectives

  • Inspect socket, context, daemon and builder identity without giving a new container access to the Docker control plane.
  • Create two disposable Buildx docker-container builder instances from a trusted controller and keep their build cache/output identities separate.
  • Distinguish a safe builder-isolation exercise from the much stronger claim that untrusted job code is isolated from the host daemon.
  • Compare host-socket, DinD, rootless and remote-BuildKit patterns without executing privileged or socket-mount shortcuts.
  • Clean up only the exact builders and files created by the lab.

1. Lab contract and safety boundary

This lab is intentionally split into trusted controller and simulated job roles. You run the Docker/Buildx commands from your normal authorized shell. No lab container receives the host Docker socket, and no lab command launches a privileged container. Two BuildKit workers model per-job build state; they do not claim that the controller’s host daemon has been isolated from itself.

Names beginning with dca35- and the local directory dca35-lab are the only disposable resources.

2. Preflight: capture platform, context, daemon and builder state

set -eu
LAB=dca35-lab
mkdir -p "$LAB/evidence" "$LAB/src" "$LAB/out-a" "$LAB/out-b"

date -u +%Y-%m-%dT%H:%M:%SZ | tee "$LAB/evidence/time-start.txt"
docker context show | tee "$LAB/evidence/context.txt"
docker version | tee "$LAB/evidence/docker-version.txt"
docker info | tee "$LAB/evidence/docker-info.txt"
docker buildx version | tee "$LAB/evidence/buildx-version.txt"
docker buildx ls | tee "$LAB/evidence/builders-before.txt"

3. Inspect the native socket if one is visible

if [ -S /var/run/docker.sock ]; then
  ls -l /var/run/docker.sock | tee dca35-lab/evidence/socket-ls.txt
  stat /var/run/docker.sock | tee dca35-lab/evidence/socket-stat.txt
else
  printf '%s
' 'No native socket visible; record context/endpoint instead.'     | tee dca35-lab/evidence/socket-note.txt
fi

This is evidence collection only. Do not change socket mode or group membership and do not bind it into a lab container.

4. Create a deterministic synthetic build context

cat > dca35-lab/src/Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.22
ARG JOB_ID
RUN printf 'job=%s
' "$JOB_ID" > /artifact.txt
FROM scratch
COPY --from=0 /artifact.txt /artifact.txt
EOF

sha256sum dca35-lab/src/Dockerfile   | tee dca35-lab/evidence/dockerfile.sha256

The final filesystem contains only /artifact.txt. No secret, registry credential, host path, or Docker socket is part of the build context.

5. Trusted controller creates two disposable builder instances

docker buildx create --name dca35-job-a --driver docker-container --bootstrap
docker buildx create --name dca35-job-b --driver docker-container --bootstrap

docker buildx inspect dca35-job-a | tee dca35-lab/evidence/builder-a.txt
docker buildx inspect dca35-job-b | tee dca35-lab/evidence/builder-b.txt

The controller is using its existing authorized Docker endpoint to provision two BuildKit workers. In a hardened CI system, untrusted repository code should not be the component that receives this controller authority.

6. Build job A into a local output directory

rm -rf dca35-lab/out-a/*
docker buildx build   --builder dca35-job-a   --progress=plain   --build-arg JOB_ID=A   --output type=local,dest=dca35-lab/out-a   dca35-lab/src   | tee dca35-lab/evidence/build-a.log

cat dca35-lab/out-a/artifact.txt   | tee dca35-lab/evidence/output-a.txt

type=local exports the final filesystem to the requested directory. It does not load a runnable image into the shared Engine image store, which keeps this exercise focused on builder state.

7. Build job B on the separate builder

rm -rf dca35-lab/out-b/*
docker buildx build   --builder dca35-job-b   --progress=plain   --build-arg JOB_ID=B   --output type=local,dest=dca35-lab/out-b   dca35-lab/src   | tee dca35-lab/evidence/build-b.log

cat dca35-lab/out-b/artifact.txt   | tee dca35-lab/evidence/output-b.txt

8. Prove builder and cache namespaces are distinct

docker buildx du --builder dca35-job-a   | tee dca35-lab/evidence/cache-a.txt

docker buildx du --builder dca35-job-b   | tee dca35-lab/evidence/cache-b.txt

docker buildx ls | tee dca35-lab/evidence/builders-after-build.txt

printf 'A='; cat dca35-lab/out-a/artifact.txt
printf 'B='; cat dca35-lab/out-b/artifact.txt

The two builder names resolve to different BuildKit worker instances and have separate cache accounting. This is useful job-level build isolation. It is not proof that repository code could not control the host daemon if you also gave that repository the Docker socket.

9. Observe the controller-side builder containers without entering them

docker ps \
  --filter 'name=buildx_buildkit_dca35-job-a' \
  --format '{{.ID}} {{.Names}} {{.Image}} {{.Status}}'   | tee dca35-lab/evidence/controller-builder-a-container.txt

docker ps \
  --filter 'name=buildx_buildkit_dca35-job-b' \
  --format '{{.ID}} {{.Names}} {{.Image}} {{.Status}}'   | tee dca35-lab/evidence/controller-builder-b-container.txt

This observation is performed by the trusted controller. An untrusted job should not need host-daemon listing rights merely to submit a build to a remote or pre-provisioned BuildKit endpoint.

10. Architecture comparison without executing dangerous variants

Pattern Would this lab run it? Why / safer teaching path
Host socket mounted into a job No Inspect socket authority and discuss API capability; do not hand host control to a lab container
Classic privileged DinD No Model architecture and provider guidance; no need to disable container security mechanisms for this lesson
Rootless DinD inside Docker No mandatory run Docker documents that its containerized rootless DinD image still needs privileged outer execution; use rootless host/VM or provider-managed isolation when evaluating it
Rootless BuildKit on Linux Optional A non-root BuildKit daemon can provide a narrower build endpoint when RootlessKit/user namespaces are available
Remote BuildKit Design exercise Use TLS/Unix/SSH-style protected transport and per-job credentials; no remote service is required for the mandatory lab

11. Optional Linux extension: inspect rootless BuildKit prerequisites

command -v rootlesskit || true
command -v buildkitd || true
command -v buildctl || true
id
[ -r /etc/subuid ] && grep "^$(id -un):" /etc/subuid || true
[ -r /etc/subgid ] && grep "^$(id -gn):" /etc/subgid || true

If these prerequisites are not present, stop at inspection. Do not change kernel/user-namespace policy merely to complete the chapter. A real CI platform can provision an isolated rootless BuildKit worker outside the job instead.

12. Exact cleanup only

docker buildx rm dca35-job-a dca35-job-b

docker buildx ls | tee dca35-lab/evidence/builders-after-cleanup.txt
find dca35-lab -maxdepth 2 -type f -print | sort   | tee dca35-lab/evidence/files-before-local-delete.txt

The evidence directory is intentionally retained for review. After reviewing it, delete only dca35-lab with your normal file manager or rm -rf dca35-lab while you are in the directory that contains that exact lab path. No Docker prune operation is required.

13. Challenge: choose the layer, not the memorized command

Your organization accepts public pull requests. The current runner launches a job container and that job can list unrelated long-lived containers. Which layer is wrong?

Answer target: this is not an image-build bug. The CI job has been connected to a shared daemon/control plane. Fix the runner/build-service architecture so untrusted code receives an isolated or narrow build endpoint, then scope cache and credentials to that worker. Do not attempt to hide unrelated containers with naming conventions.

Knowledge check

What exactly did the two-builder lab prove?

Why use local build outputs instead of loading both results into the Engine image store?

Why is rootless DinD not the mandatory runnable path here?

What is a safer control-plane split for untrusted build code?

What cleanup operation is required for Docker state?

Next lesson

Next: Docker Socket Security, Docker-in-Docker, Socket Mounting, Build Services, and CI Isolation Tradeoffs: 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

Lab baseline: 2026-09-22. The mandatory path uses local Buildx docker-container builders as an isolation model and deliberately avoids socket mounts and privileged nested daemons. It is a build-state isolation exercise, not a claim that docker-container removes controller daemon authority.

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.