Chapter 15Lesson 05~245 minutes

Checkpoint Lab — Service Containers, Job Containers, Docker Builds, and Integration-Test Environments

The checkpoint creates one application and one Redis service, deliberately uses the wrong service address in the first run, preserves the resulting network evidence, repairs the address in a second run, then builds the same application into a local image and verifies its content-addressed local image ID before cleanup.

Checkpoint labNetwork repairLocal image IDTeardownEvidence packet

Learning objectives

  • Create one disposable application and one healthy Redis service with explicit host-job topology.
  • Predict and preserve a first run that fails because the host process incorrectly uses the service-container hostname.
  • Repair only the network address and independently verify the Redis interaction in a second run.
  • Build the application into a local Docker image, compare IID-file and Docker-inspected identities and run a bounded image self-test.
  • Produce an evidence packet covering event/SHA, runner/Docker state, service/image identity, network target, failure/repair and teardown.

1. Checkpoint scenario and safety contract

Create a disposable repository named gha-ch15-checkpoint. The lab uses a manual choice input network_mode with values broken and repaired. Both runs use the same workflow revision, ubuntu-24.04, permissions: {}, a Redis service and fake key/value data. No registry token, package publication, cloud resource or production endpoint is permitted.

Run A intentionally fails at application connectivity. Preserve its run ID/attempt, SHA, service health evidence and connection error. Run B changes only the typed input to repaired; it should connect successfully and continue to the local Docker build.

2. Make predictions before execution

Prediction Run A — broken Run B — repaired
Redis service healthy before application step healthy before application step
Application endpoint redis:6379 from runner host — wrong topology 127.0.0.1:6379 — mapped host port
Integration conclusion fails with DNS/name-resolution or connection error succeeds with PING/SET/GET evidence
Docker build does not run because prior step failed builds local run-scoped tag
Local image identity not produced IID file must equal Docker-inspected sha256: image ID
Registry/external state none none; no image push

3. Exact checkpoint workflow

name: chapter15-checkpoint
on:
  workflow_dispatch:
    inputs:
      network_mode:
        description: Deliberately broken or repaired service address
        required: true
        type: choice
        options: [broken, repaired]
        default: broken
permissions: {}

jobs:
  integration-and-build:
    runs-on: ubuntu-24.04
    services:
      redis:
        image: redis:7.4-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 5s
          --health-timeout 3s
          --health-retries 10
    steps:
      - name: Record runner, Docker and service evidence
        shell: bash
        run: |
          echo "run=$GITHUB_RUN_ID attempt=$GITHUB_RUN_ATTEMPT sha=$GITHUB_SHA"
          echo "runner=$RUNNER_OS/$RUNNER_ARCH"
          docker version
          docker ps --format 'table {{.ID}}	{{.Image}}	{{.Names}}	{{.Status}}	{{.Ports}}'
          docker image inspect redis:7.4-alpine             --format 'redis_id={{.Id}} repo_digests={{json .RepoDigests}}' || true

      - name: Create one tiny application and Docker build context
        shell: bash
        run: |
          mkdir -p .lab
          cat > .lab/app.py <<'PY'
          import os, socket
          host=os.environ.get('REDIS_HOST','127.0.0.1')
          port=int(os.environ.get('REDIS_PORT','6379'))
          def command(*parts):
              payload=f"*{len(parts)}\r\n" + ''.join(f"${len(p)}\r\n{p}\r\n" for p in parts)
              with socket.create_connection((host,port), timeout=4) as s:
                  s.sendall(payload.encode())
                  return s.recv(4096).decode(errors='replace')
          print('target', f'{host}:{port}')
          print('PING', command('PING').strip())
          print('SET', command('SET','chapter15:checkpoint','verified').strip())
          print('GET', command('GET','chapter15:checkpoint').strip())
          PY
          cat > .lab/Dockerfile <<'DOCKER'
          FROM python:3.13-slim
          WORKDIR /app
          COPY app.py /app/app.py
          CMD ["python", "-c", "import platform; print('chapter15 image verified'); print(platform.python_version())"]
          DOCKER
          printf 'image-id.txt
' > .lab/.dockerignore

      - name: Exercise application against service
        shell: bash
        env:
          NETWORK_MODE: ${{ inputs.network_mode }}
        run: |
          if [[ "$NETWORK_MODE" == 'broken' ]]; then
            export REDIS_HOST='redis'
          else
            export REDIS_HOST='127.0.0.1'
          fi
          export REDIS_PORT='6379'
          python3 .lab/app.py

      - name: Build local application image
        shell: bash
        run: |
          tag="gha-ch15-checkpoint:${GITHUB_RUN_ID}"
          docker build --pull --iidfile .lab/image-id.txt -t "$tag" .lab
          iid_file="$(cat .lab/image-id.txt)"
          iid_inspect="$(docker image inspect "$tag" --format '{{.Id}}')"
          printf 'tag=%s
iid_file=%s
iid_inspect=%s
' "$tag" "$iid_file" "$iid_inspect"
          test "$iid_file" = "$iid_inspect"
          docker run --rm "$tag"

      - name: Write evidence summary
        if: ${{ always() }}
        shell: bash
        env:
          NETWORK_MODE: ${{ inputs.network_mode }}
        run: |
          {
            echo '### Chapter 15 checkpoint'
            echo "- run: $GITHUB_RUN_ID attempt $GITHUB_RUN_ATTEMPT"
            echo "- source SHA: $GITHUB_SHA"
            echo "- mode: $NETWORK_MODE"
            echo "- runner: $RUNNER_OS/$RUNNER_ARCH"
            if [[ -f .lab/image-id.txt ]]; then
              echo "- local image ID: $(cat .lab/image-id.txt)"
            else
              echo '- local image ID: not produced because integration did not pass'
            fi
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Bounded local-image cleanup
        if: ${{ always() }}
        shell: bash
        run: |
          tag="gha-ch15-checkpoint:${GITHUB_RUN_ID}"
          docker image rm "$tag" 2>/dev/null || true

4. Run A — preserve the network failure

Dispatch with network_mode=broken. The Redis service has host port 6379 published, but the application is a runner-host process and tries hostname redis. That service label is meaningful on GitHub's container network, not as a normal host DNS name.

Expected result: service initialization succeeds, runner/Docker evidence is recorded, application prints target redis:6379 and fails before the build step. The summary still runs through always() and records that no local image ID was produced. Preserve this exact run and do not edit it away.

5. Diagnose the failure causally

Use the evidence-first sequence: confirm run/SHA and unchanged workflow revision; confirm the job has no container: field, so the client runs on the host; confirm Redis health and port mapping; compare the actual client target with the topology rule. The smallest causal correction is the client hostname.

Do not repair by adding --network host, privileged mode, Docker socket mounts, random DNS entries or extra credentials. Those change unrelated trust boundaries.

6. Run B — repair only the address

Dispatch the same revision with network_mode=repaired. The application now connects to 127.0.0.1:6379. PING, SET and GET should succeed, proving the mapped host path.

Only after the integration invariant passes does the build step execute. This ordering prevents a failed integration run from producing a misleading “verified” image in the checkpoint.

7. Build and verify local image identity

docker build --iidfile records the built image's local ID. The workflow independently inspects the run-scoped tag and requires the IDs to match exactly. The subsequent docker run --rm is a bounded runtime smoke test of the built image.

The local image has no registry digest because the workflow never pushes it. Record this explicitly instead of inventing a registry provenance claim. The base python:3.13-slim may resolve to a repository digest during the pull; production builds should pin that digest when base-image immutability is required.

8. Required evidence packet

Evidence Required record
Workflow identity workflow filename/revision plus permissions: {}
Run identity Run A and B IDs/attempts and the exact source SHA
Runner/toolchain ubuntu-24.04, runner OS/arch, Docker client/server versions
Service state Redis image reference, observed container/image ID or RepoDigest where visible, health status and port mapping
Network prediction why host process should use localhost and job-container process would use service label
First failure Run A target redis:6379 and resulting connection/DNS error; build skipped
Repair Run B target 127.0.0.1:6379 plus PING/SET/GET success
Build identity Dockerfile/context, run-scoped tag, IID file, independently inspected local sha256: ID
Registry/deployment state explicitly none — no login, push, environment approval or external deployment
Cleanup run-scoped local image removal plus GitHub-managed service/hosted-runner teardown

9. Verification checklist

  • Run A and Run B use the same workflow revision and source SHA where practical; if the SHA differs, document why and compare the exact workflow text.
  • Run A preserves healthy-service evidence and the wrong application target before repair.
  • Run B proves PING/SET/GET against fake disposable Redis data.
  • The build executes only after repaired integration succeeds.
  • IID-file and Docker-inspected image ID match exactly and begin with sha256:.
  • No registry credential appears anywhere and no registry/release/package is mutated.
  • The run-bounded local image is removed explicitly; service containers are left to GitHub's managed teardown.

10. Free local simulation and rollback

On any disposable Linux machine with Docker, run Redis with -p 6379:6379 and execute the Python application on the host twice: first with REDIS_HOST=redis to reproduce the wrong-hostname failure, then with REDIS_HOST=127.0.0.1. Build the same Dockerfile with --iidfile, compare its inspected ID and remove the local image/container.

Delete the throwaway repository after retaining only non-sensitive notes if desired. There are no registry, cloud, Kubernetes or external deployment resources to roll back.

11. What Chapter 15 adds to the operating model

You can now model containerized CI as explicit topology: runner host, job container, service network, Docker daemon, build context, image identity and teardown. You can diagnose address/health failures without granting new privilege and can distinguish a local tested image from a published/deployed artifact. Chapter 16 moves to Reusable Workflows, workflow_call, Inputs, Secrets, Outputs, and Nesting, where the next boundary is automation reuse rather than container isolation.

Knowledge check

Why is Run A valuable even though it is intentionally broken?

What is the smallest repair between Run A and Run B?

Why is the build step not guarded with always()?

What identity is verified for the locally built image?

What is the natural next chapter boundary?

Next chapter concept

Reuse automation without hiding interfaces

Chapter 16 turns workflow reuse into explicit contracts for inputs, secrets, outputs and nested calls.

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked on 2026-09-09. GitHub job containers, service containers and Docker container actions require a Linux runner; on GitHub-hosted runners that means Ubuntu. When a job runs on the host, mapped service ports are reached through localhost; when the job itself runs in a container, service containers share a Docker network and are reached by their service labels without host-port publication. The mandatory labs target ubuntu-24.04, use public version-family images redis:7.4-alpine and python:3.13-slim, and record resolved image metadata rather than claiming those tags are immutable. They use the Docker CLI already present on the hosted Ubuntu runner and perform no registry login or push. Optional production Buildx examples should pin the full SHAs listed above. The checkpoint deliberately does not use Buildx/login/build-push actions or registry credentials; it stops at a locally verified image ID to keep mandatory learning free and bounded.

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.