Docker / CI & isolation
Docker in CI: The Real Cost of Docker-in-Docker
Why teams use DinD, what daemon access exposes, and how to make container tests reliable.
Docker-in-Docker (DinD) can make CI feel simple: start a job, build an image, run container-based tests. The convenience comes with a real isolation decision. Privileged DinD can weaken the boundary around the runner; mounting the host Docker socket can give a build step powerful control over the host daemon.
Teams still choose these patterns because they support familiar workflows. The important thing is to make the choice explicit, restrict which code can use it, and avoid a “green” result when container tests never ran.
First, know which daemon the job is using
“Docker in CI” does not always mean DinD. Some runners expose a Docker daemon to the job; DinD starts a separate Docker daemon inside a service container. A mounted /var/run/docker.sock is a third pattern: the job’s Docker CLI talks to the host daemon. These setups have different isolation properties. Document the runner architecture before relying on it.
Verify the daemon before running tests
Make readiness an explicit step. If the daemon is missing or unresponsive, stop the job before it reports container-dependent tests as successful:
# Run only on a runner configured to provide Docker access.
set -eu
docker version
docker info
IMAGE="aceu-trust-demo:${GITHUB_SHA:-local}"
docker build --tag "$IMAGE" .
docker run --rm "$IMAGE" pytest -q
For an actual DinD service, the job also needs to wait for that service’s daemon to become available. A fixed sleep is brittle; use a bounded readiness loop and fail with a clear message if the daemon never responds.
Do not silently skip container-dependent tests
If the daemon is a prerequisite, the test should fail when Docker is unavailable. Avoid code that catches every exception and quietly returns a passing or skipped result in a required CI job:
import subprocess
def docker_available() -> bool:
try:
subprocess.run(
["docker", "info"],
check=True,
capture_output=True,
text=True,
timeout=10,
)
return True
except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
return False
def test_containerized_health_check_requires_docker():
assert docker_available(), "Docker daemon is required for this test"
# Continue with the containerized test flow.
Tag Docker-dependent tests so developers can distinguish them from pure unit tests, but keep them required in the CI path that claims to validate the container.
Choose the access pattern with eyes open
- Privileged DinD: familiar and flexible, but grants elevated capabilities to the service container. Restrict it to trusted workflows and isolate the runner.
- Host socket mount: avoids starting another daemon, but the mounted socket can let job code control the host Docker daemon. Treat access as highly privileged.
- Daemonless or remote builders: can reduce direct daemon exposure in some environments, but require setup and may change the build workflow.
In every case, pin build images, keep credentials scoped, avoid running untrusted pull-request code on privileged self-hosted runners, and clean up jobs so containers and volumes do not leak into later runs.
Watch for isolation leaks and flaky runners
Look for unexpected host mounts, files written outside the workspace, orphaned containers, and daemon failures. Runner fleets can have transient problems. A documented single retry can filter a brief infrastructure hiccup, but repeated failure should remain visible and investigated—not retried until a broken test turns green.
Trade-offs to discuss with the team
- Speed vs. isolation: DinD is familiar; stricter alternatives can require more setup.
- Fail closed vs. ergonomics: blocking on daemon failure catches false greens but can be frustrating when runner health is poor. Give contributors a clear error and retry path.
- Socket mount vs. privileged mode: neither is a free isolation boundary. Limit trusted code and runner scope for either design.
Try it in your CI
- Write down whether jobs use host Docker, a mounted socket, or a separate DinD service.
- Add a daemon readiness check before any container-dependent test.
- Make those tests fail if Docker is unavailable; do not silently skip them.
- Evaluate a daemonless or remote builder against your runner threat model.
- Document when one retry is appropriate and when repeated failure needs investigation.
Closing thought
DinD can be pragmatic, but it should never be invisible. Verify the daemon, make tests fail closed, and choose an isolation pattern that matches what the CI job is trusted to do.