Docker Pipeline, Containerized Build Steps, Docker Agents, Sidecars, Registries, and Image Workflows: Guided Hands-On Workflow and Core Operations
Run a controlled Docker-backed Pipeline on a dedicated trusted agent: resolve image digests, test with a disposable sidecar, build once, authenticate to a loopback registry with fake Jenkins credentials, push and capture the immutable digest, then clean exact build-owned resources.
Learning objectives
- Prepare a dedicated trusted Docker-capable agent while keeping the built-in node at zero executors.
- Resolve reviewed image tags to architecture-specific digests and execute containerized tests.
- Start a disposable sidecar/network and retain container/readiness evidence.
- Build one image candidate and push it to an authenticated local registry using fake credentials.
- Capture the registry digest and clean only resources created by this lab.
1. Disposable topology and assumptions
Use Jenkins 2.568.3 LTS on Java 21. Keep the built-in
node at 0 executors. Use one disposable worker with
labels ch19-docker trusted, one executor, a reviewed
Docker CLI/Engine, and no production cloud/signing/deployment
credentials.
At the chapter timestamp, Docker Engine 29’s current patch line
includes 29.8.1. Record the actual
docker version from your worker rather than silently
assuming that exact patch.
2. Preflight: prove Jenkins placement and Docker endpoint
pipeline {
agent { label 'ch19-docker && trusted' }
options { timeout(time: 15, unit: 'MINUTES') }
stages {
stage('Preflight') {
steps {
sh '''
set -eu
printf 'job=%s build=%s node=%s workspace=%s\n' \
"$JOB_NAME" "$BUILD_NUMBER" "$NODE_NAME" "$WORKSPACE"
docker version
docker context show
docker info --format 'server={{.ServerVersion}} driver={{.Driver}} security={{json .SecurityOptions}}'
'''
}
}
}
}
Capture the current Docker Pipeline plugin version from Jenkins plugin inventory. If the job lands on an unexpected node or the CLI reaches an unexpected Docker context, fix placement/endpoint first.
3. Resolve input tags before execution
Reviewed lab tags are python:3.13-alpine3.24,
alpine:3.24.1, registry:3.1.1, and
httpd:2.4.68-alpine3.24. Pull and record the actual
digests:
set -eu
for img in \
python:3.13-alpine3.24 \
alpine:3.24.1 \
registry:3.1.1 \
httpd:2.4.68-alpine3.24
do
docker pull "$img"
docker image inspect --format '{{join .RepoDigests "\\n"}}' "$img"
done | tee ch19-input-images.txt
Set a non-secret Pipeline/environment variable
CH19_PYTHON_IMAGE to the exact reviewed
python@sha256:… identity before running the test
stages.
4. Synthetic source
mkdir -p app
printf '%s\n' 'hello-from-ch19' > app/message.txt
cat > app/check.py <<'PYCODE'
from pathlib import Path
value = Path('app/message.txt').read_text().strip()
assert value == 'hello-from-ch19'
print('source-check=ok')
PYCODE
Commit these files and the Jenkinsfile to a disposable SCM repository if possible so the build records an exact source SHA. Otherwise record a synthetic content hash and state that limitation.
5. Execute a step inside the pinned image
node('ch19-docker && trusted') {
stage('Container test') {
if (!env.CH19_PYTHON_IMAGE?.startsWith('python@sha256:')) {
error('CH19_PYTHON_IMAGE must be an approved digest reference')
}
docker.image(env.CH19_PYTHON_IMAGE).inside {
sh 'python --version'
sh 'python app/check.py'
sh 'printf "container_host=%s\\n" "$(cat /etc/hostname)" > container-test.txt'
}
archiveArtifacts artifacts: 'container-test.txt', fingerprint: true
}
}
The image digest is input evidence. The mounted Jenkins workspace
carries app/ into the test container; deleting the
container does not automatically delete the workspace.
6. Add a build-unique sidecar/network
node('ch19-docker && trusted') {
def safeBuild = env.BUILD_TAG.replaceAll(/[^A-Za-z0-9_.-]/, '-')
def net = "ch19-${safeBuild}"
sh "docker network create ${net}"
try {
docker.image(env.CH19_PYTHON_IMAGE).withRun(
"--network ${net} --network-alias ch19-api -v ${pwd()}/app:/srv:ro python -m http.server 8080 --directory /srv"
) { svc ->
echo "sidecar-id=${svc.id}"
retry(10) {
sleep 1
sh "docker run --rm --network ${net} ${env.CH19_PYTHON_IMAGE} python -c \"import urllib.request; assert urllib.request.urlopen('http://ch19-api:8080/message.txt', timeout=2).read().decode().strip() == 'hello-from-ch19'\""
}
sh "docker logs ${svc.id} > sidecar.log 2>&1 || true"
}
} finally {
sh "docker network rm ${net} || true"
}
}
withRun bounds the sidecar container; the explicit
network has its own cleanup. Preserve IDs/logs before cleanup if the
readiness test fails.
7. Start an authenticated loopback registry with fake credentials
Create auth state outside the Jenkins workspace:
set -eu
LABROOT=/tmp/ch19-registry-lab
rm -rf "$LABROOT"
install -d -m 700 "$LABROOT/auth" "$LABROOT/data"
docker run --rm --entrypoint htpasswd httpd:2.4.68-alpine3.24 \
-Bbn ch19user 'fake-registry-pass-19' > "$LABROOT/auth/htpasswd"
chmod 600 "$LABROOT/auth/htpasswd"
docker run -d --name ch19-registry \
-p 127.0.0.1:5000:5000 \
-e REGISTRY_AUTH=htpasswd \
-e 'REGISTRY_AUTH_HTPASSWD_REALM=ch19-local' \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
-v "$LABROOT/auth:/auth:ro" \
-v "$LABROOT/data:/var/lib/registry" \
registry:3.1.1
Create a Jenkins Username/Password credential
ch19-registry-local with username
ch19user and the fake password. Record only the
credential ID.
8. Build the candidate exactly once
FROM alpine:3.24.1
ARG SOURCE_SHA=unknown
ARG JENKINS_BUILD=unknown
LABEL org.opencontainers.image.revision=$SOURCE_SHA
LABEL io.jenkins.build=$JENKINS_BUILD
COPY app/message.txt /opt/ch19/message.txt
CMD ["cat", "/opt/ch19/message.txt"]
stage('Build image once') {
steps {
sh '''
set -eu
IMAGE_LOCAL="ch19-demo:${BUILD_NUMBER}"
docker build \
--build-arg SOURCE_SHA="${GIT_COMMIT:-synthetic}" \
--build-arg JENKINS_BUILD="$BUILD_URL" \
-t "$IMAGE_LOCAL" .
docker image inspect "$IMAGE_LOCAL" > image-local-inspect.json
'''
}
}
Do not run another build during promotion. The exact content pushed from this candidate becomes the release identity.
9. Authenticate and push without logging the password
withCredentials([usernamePassword(
credentialsId: 'ch19-registry-local',
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS'
)]) {
sh '''
set -eu
REGISTRY=127.0.0.1:5000
REPO="$REGISTRY/ch19/demo"
REF="$REPO:${BUILD_NUMBER}"
printf '%s' "$REG_PASS" | docker login "$REGISTRY" --username "$REG_USER" --password-stdin >/dev/null
docker tag "ch19-demo:${BUILD_NUMBER}" "$REF"
docker push "$REF" | tee push.log
DIGEST=$(awk '/digest:/ {print $2}' push.log | tail -1)
test -n "$DIGEST"
printf 'repository=%s\ndigest=%s\nproducer_build=%s\nsource=%s\n' \
"$REPO" "$DIGEST" "$BUILD_URL" "${GIT_COMMIT:-synthetic}" > image-digest-evidence.txt
docker pull "$REPO@$DIGEST"
docker logout "$REGISTRY" >/dev/null
'''
}
archiveArtifacts artifacts: 'push.log,image-digest-evidence.txt,image-local-inspect.json', fingerprint: true
The registry-reported digest is the retained candidate identity. The Jenkins artifact fingerprint here tracks the evidence files; it is not a substitute for the OCI image digest.
10. Layer-selection challenge
The agent is online and local docker run works, but the
private image push is denied. Do you increase executors, restart the
agent, or inspect registry identity/authorization?
Correct layer: registry/credential/repository authorization. Preserve the build, node, daemon and denied response. Verify endpoint, credential ID scope, username, repository path and allowed action. Do not solve authorization by granting broad registry admin access.
11. Guided cleanup
docker rm -f ch19-registry 2>/dev/null || true
# Inspect first; remove only build-owned names/IDs.
docker network ls --format '{{.Name}}' | grep '^ch19-' || true
docker image rm "ch19-demo:${BUILD_NUMBER}" 2>/dev/null || true
rm -rf /tmp/ch19-registry-lab
Delete the fake Jenkins credential if it was created only for this chapter. Never use a broad global prune on a shared worker.
Knowledge check
Answer before revealing the explanation.
1. Why resolve the test image tag to a digest before the run?
Tags can move. The digest records the exact architecture-specific content actually used.
2. Why is the Docker agent marked trusted?
The job can control a Docker engine and therefore has broader host/resource authority than ordinary shell execution; untrusted PR code must not receive it.
3. Why bind the registry only to loopback?
The mandatory registry is a local disposable authentication simulation, not production. Loopback limits exposure while preserving real push/login semantics.
4. What should be captured after docker push?
Capture the registry-reported digest, repository, source SHA, producer build, and push result—not merely the local tag.
5. What makes cleanup safe?
Use build-unique names/IDs and remove only resources created by the lab after first-failure evidence is preserved; never use broad prune as a shortcut.
Official references and version notes
-
Jenkins LTS changelog
— baseline
Jenkins 2.568.3 LTS, tested with Java 21 and 25; labs use Java 21 for Jenkins components. - Using Docker with Pipeline — Docker agents, workspace synchronization, multiple containers, sidecars, builds, remote servers, and custom registries.
-
Docker Pipeline plugin
— reviewed version
653.v2f2c08eff0ec, requires Jenkins 2.541.3, and is currently marked “up for adoption”. - Docker Pipeline steps — current step reference; deprecated Docker fingerprint steps are not used as authenticity/provenance evidence.
- Docker Engine security, protect daemon access, and rootless mode.
-
Docker Engine 29 release notes
— current release family at the chapter timestamp;
29.8.1was released 2026-09-15. -
Distribution Registry official image
— local lab baseline
registry:3.1.1. -
Python,
Alpine, and
httpd
official images — reviewed lab tags
python:3.13-alpine3.24,alpine:3.24.1, andhttpd:2.4.68-alpine3.24; resolve actual architecture-specific digests before execution.
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.