Chapter 19Lesson 02~190 minutes

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.

Hands-onPinned containersLocal registryFake credentialsSidecarCleanup

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
Local simulation only: this loopback registry is disposable. Do not expose it to a network or copy this HTTP setup to production.

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.

Next lesson

Configuration, Design Choices, and Tradeoffs

Compare plugin versus CLI, rootful versus rootless/remote builders, Docker-agent placement, workspace/caching, and digest promotion.

Knowledge check

Answer before revealing the explanation.

1. Why resolve the test image tag to a digest before the run?

2. Why is the Docker agent marked trusted?

3. Why bind the registry only to loopback?

4. What should be captured after docker push?

5. What makes cleanup safe?

Official references and version notes

Assumption timestamp: 2026-09-17. Recheck Jenkins LTS/Java, Docker Pipeline health/version/dependencies, Docker Engine security/release notes, and all image tags/digests before repeating later.

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.