Chapter 15Lesson 01~165 minutes

Service Containers, Job Containers, Docker Builds, and Integration-Test Environments: Core Concepts and Mental Model

Chapter 14 treated cache state as optional performance input. Chapter 15 moves the boundary inside one workflow job: the runner host, an optional job container, service containers, Docker networks, the Docker daemon and image build context are distinct execution states. Reproducible integration tests begin by identifying which process lives in which namespace.

Runner hostJob containerService containerDocker networkImage identity

Learning objectives

  • Distinguish runner-host, job-container, service-container, Docker-daemon and build-context state.
  • Predict host-versus-container service addressing before running an integration test.
  • Explain why a tag, repository digest and local Docker image ID are different identities.
  • Inspect runner, Docker, network, volume, image and teardown evidence without mutating production systems.
  • Identify credential, privilege, port-exposure and mutable-image risks in containerized CI.

1. The practical problem: “it runs in Docker” still leaves several different computers and networks

A GitHub Actions job can execute directly on the runner host, inside a job container, or invoke container actions. The same job can also have one or more service containers. Separately, the runner may expose a Docker daemon that builds images from a filesystem context. These layers are related but they are not interchangeable.

The most common integration-test error is to carry an address or filesystem assumption from one layer into another. localhost means “this network namespace.” A file in the runner workspace is not automatically a file inside an arbitrary service container. A Docker image tag is not proof of which manifest was pulled. A successful container health check is not proof that the application used the correct hostname.

2. Causal model: runner host → job/service topology → processes → Docker build → teardown

GitHub first assigns the job to a Linux runner. If container: is configured, the runner creates a job container. For each services: entry it creates a service container and manages its lifecycle. GitHub then creates networking appropriate to the topology. Workflow steps execute either on the host or inside the job container. A separate Docker CLI invocation can ask the host Docker daemon to build an image from a selected build context. At job completion, GitHub tears down the managed job/service containers; an ephemeral hosted runner is later discarded.

Containerized Actions execution boundaries
flowchart TD
  A[Workflow event + exact revision] --> B[Linux runner assigned]
  B --> C{Job has container?}
  C -->|No| D[Steps execute on runner host]
  C -->|Yes| E[Job container created]
  B --> F[Service containers created]
  F --> G[Docker service network / mapped ports]
  D --> H[Host process connects through localhost + mapped port]
  E --> I[Container process connects by service hostname]
  D --> J[Docker CLI talks to host daemon]
  J --> K[Build context + Dockerfile -> local image ID]
  H --> L[Test evidence]
  I --> L
  K --> L
  L --> M[Managed containers stop + hosted VM teardown]

The arrows are causal: runner choice determines whether Docker-backed container features are available; topology determines addressability; build context determines image contents; teardown determines which local state is guaranteed to disappear.

3. Define each layer before troubleshooting it

Layer What it owns Evidence to record
Runner host OS, Docker daemon/client, workspace/temp paths, host network RUNNER_OS, docker version, workspace path, run/job ID
Job container shell/runtime for ordinary run steps, container filesystem and network namespace container image reference, default shell, runtime version, hostname
Service container database/cache/queue process used by the job service label, image, health state, published ports, container ID when observable
Docker network name resolution and port reachability between containers/host service hostname, mapped host port, connection target used by app
Docker daemon/build image pull/build cache, build context, local image store Docker version, Dockerfile, context path, base-image metadata, IID file
Image identity tag/reference, registry digest or local content-addressed ID tag, RepoDigests when pulled, sha256: local image ID
Teardown managed container stop/removal and hosted-VM disposal final job conclusion, explicit lab cleanup, assumption that hosted VM is ephemeral

4. Host job and job-container networking are deliberately different

When ordinary steps run directly on the runner host, service containers do not automatically expose their ports to host processes. The workflow must publish the service port. The host application then connects to localhost or 127.0.0.1 using the mapped host port.

When the job itself runs in a container, GitHub places the job container and its service containers on the same user-defined Docker network. All service ports are available within that network and the service label becomes the hostname. A Redis service named redis is therefore reached at redis:6379. Publishing that port to the host is unnecessary unless some host process genuinely needs it.

Topology rule

Do not memorize “Redis is localhost” or “Redis is redis.” Derive the address from where the client process actually runs.

5. A job container changes more than the image

With jobs.<id>.container, normal run steps execute inside that container. GitHub documents sh as the default shell for container jobs rather than bash, so Bash-specific syntax must either request Bash explicitly in an image that provides it or use portable shell code.

Container credentials, environment variables, volume mounts and Docker create options become workflow configuration state. The --network and --entrypoint options are not supported through the job-container options field. That is another reason to let GitHub manage the network rather than trying to replace it.

6. A service container is supporting infrastructure, not the job environment

A service container usually hosts a dependency such as PostgreSQL, Redis or an HTTP test double. Its health check answers “is this service process ready enough to accept work?” It does not install client libraries into the job and it does not make the service state persistent beyond the job.

Service volumes can make selected data visible for the lifetime of the job, but the reproducible default is disposable state seeded by the workflow. Do not treat a service volume on a hosted runner as a database backup or release artifact.

7. Docker build context is an input boundary

docker build PATH sends a selected filesystem context to the Docker builder together with the Dockerfile. Files excluded by .dockerignore are not part of the context. Files outside the context cannot be copied by a normal Dockerfile. Therefore the build context and ignore rules are part of reproducibility and security evidence.

The Docker daemon is separate from the process that invokes the CLI. On GitHub-hosted Ubuntu runners, Docker tooling is available, but exact daemon/client/buildx versions can change with the runner image. Record them in evidence rather than assuming a particular patch version.

8. Tag, registry digest and local image ID answer different questions

Identity Question answered Important limitation
Tag such as python:3.13-slim Which named reference did the workflow request? tag owners can move the tag to a different manifest
Registry digest name@sha256:… Which immutable registry manifest/content was selected? must be recorded or pinned; architecture-specific manifests can still matter
Local image ID sha256:… Which image configuration/content identity exists in this Docker daemon? a locally built image has no registry RepoDigest until it is pushed
Workflow run + source SHA Which automation/revision produced or used the image? does not itself prove image content; pair it with image identity

For production base images, digest pinning removes tag movement from the build input. For this beginner lab, version-family tags keep the exercise portable, and the workflow records the resolved local/pulled identities so the limitation is explicit.

9. Read-only inspection before mutation

echo "run=$GITHUB_RUN_ID attempt=$GITHUB_RUN_ATTEMPT sha=$GITHUB_SHA"
echo "runner=$RUNNER_OS/$RUNNER_ARCH"
docker version
docker info --format 'driver={{.Driver}} root={{.DockerRootDir}}'
docker ps --format 'table {{.ID}}	{{.Image}}	{{.Names}}	{{.Status}}	{{.Ports}}'
docker image ls --digests --format 'table {{.Repository}}	{{.Tag}}	{{.Digest}}	{{.ID}}'

These commands inspect the disposable runner only. They do not authorize deleting arbitrary containers or images on a long-lived self-hosted runner. On shared self-hosted infrastructure, inventory and cleanup policy belong to the runner operator.

10. Security and teardown boundaries

Registry credentials should never be embedded in a Dockerfile, image layer, command argument or build context. If a private registry is required later, use a narrowly scoped secret or federation mechanism and a reviewed login action; do not print the token. Likewise, --privileged and Docker-socket mounts grant powerful host-adjacent capability and should not be casual CI defaults.

Container teardown is not the same as credential revocation, artifact deletion or external deployment rollback. GitHub can remove a service container while a credential exposed to its process remains valid in an external system. Keep container lifecycle, secret lifecycle and external side effects as separate evidence fields.

Knowledge check

Why does localhost:6379 work in a host job but usually fail from a job container?

Does a healthy Redis service prove the application used the right hostname?

What does a local Docker image ID prove?

Why is a version-family tag not a reproducibility guarantee?

What must be true to use GitHub job containers or service containers?

Next lesson

Run the same dependency through both supported topologies

Lesson 2 makes the host-versus-container addressing rule observable and then builds a local image without a registry.

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.

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.