Jenkins Pipeline docker agent and Docker daemon: image pull policy and host dependency reproducibility
0 reputation · 15 Jan 2025, 01:34 UTC
0 reputation · 15 Jan 2025, 01:34 UTC
The agent { docker { ... } } directive in Jenkins Pipeline delegates container provisioning to the Docker daemon available on the selected agent. This creates an integration boundary between the Jenkins controller's pipeline execution model and the host-level Docker runtime, where reproducibility depends on factors outside Jenkins' direct control.
The goal is a repeatable development environment where every build uses the exact same container image and runtime characteristics regardless of which agent executes the pipeline. Current constraints include: mutable image tags (e.g., latest) resolve to different digests over time; Jenkins does not enforce --pull=always by default, so cached layers may stale; Docker daemon version, host kernel, and mounted volumes differ across agents; and the resolved image digest is not recorded in build metadata, preventing post-build audit without extra scripting. The choice between Docker-in-Docker and host socket binding further changes isolation guarantees, yet Jenkins documentation leaves this architectural decision to the user.
alwaysPull, args, reuseNode) and agent-side daemon configuration yields deterministic image resolution across heterogeneous agents?30275 reputation · 15 Jan 2025, 02:18 UTC
alwaysPull true: This forces the daemon to check the registry for a newer digest of the tag, preventing the use of stale local layers on specific agents.image:latest, use the SHA256 digest (e.g., image@sha256:abcd...). This is the only guarantee of binary-level reproducibility.reuseNode false: While this speeds up builds, setting this to false ensures a clean container is destroyed and re-constructed for every stage, preventing state leakage between steps.
agent {
docker {
image 'my-app:1.2.3@sha256:75b1...'
alwaysPull true
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
script block within your stage to export an environment variable:
stage('Identify Image') {
steps {
script {
// Capture the actual digest of the image currently running
def digest = sh(script: "docker inspect --format='{{.RepoDigests}}' .", returnStdout: true).trim()
env.IMAGE_DIGEST = digest
echo "Building with digest: ${env.IMAGE_DIGEST}"
}
}
}
| Feature | Host Socket /var/run/docker.sock | Docker-in-Docker (DinD) |
|---|---|---|
| Isolation | Low (Container controls host daemon) | High (Nested daemon) |
| Performance | Fast (Uses host cache) | Slower (Requires fresh layer pulls) |
| Reproducibility | Depends on host kernel/daemon | Higher (Self-contained environment) |
Recommendation: For mixed daemon versions, DinD is generally preferred for reproducibility because it isolates the build environment from the host's Docker version. However, if performance is critical, use host socket binding but strictly enforce alwaysPull true and digest pinning to mitigate host-level drift.
Note: This assumes your Jenkins agents are running on Linux-based nodes where Docker is installed.
Use comments to ask for clarification. Post a solution as an answer.
30,275 reputation · 15 Jan 2025, 05:46 UTC
Instead of wrapping a sh step to run docker inspect, the Jenkins Docker plugin exposes the resolved image identifier through the Image object returned by docker.image(). You can capture it directly in Groovy:
def img = docker.image('my-app:1.2.3@sha256:abcd...') img.inside { // build steps } // after the container stops, the image ID is still available echo "Built with image ID: ${img.id}"This ID corresponds to the exact digest the daemon resolved, guaranteeing the same value across agents when you use an immutable tag or digest and enable
alwaysPull true. It avoids extra shell invocations and works regardless of thereuseNodesetting.