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.
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.
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.
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?
Because localhost resolves inside the current network namespace. A job container must normally address the sibling service by its service label, such as redis.
Does a healthy Redis service prove the application used the right hostname?
No. Health proves the service process is ready; the application can still use a wrong DNS name, port or network path.
What does a local Docker image ID prove?
It identifies the locally built image content/configuration in that Docker daemon. It is not a registry digest until the image is pushed.
Why is a version-family tag not a reproducibility guarantee?
Because a registry owner can move the tag to another manifest. Record or pin a digest when immutable base-image identity matters.
What must be true to use GitHub job containers or service containers?
The job must run on Linux; GitHub-hosted workflows should use an Ubuntu runner, while self-hosted runners need Linux with Docker installed.
Official references and version notes
- GitHub Docs — workflow syntax: job containers and services — Linux requirement, shells, volumes, options and service networking.
- GitHub Docs — Redis service containers — host versus job-container topology and health checks.
- GitHub Docs — run jobs in a container — container credentials, ports, volumes and default shell behavior.
-
docker/setup-buildx-action v4.1.0
— current optional Buildx setup action, production reference
d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5. -
docker/build-push-action v7.3.0
— current optional Docker build action, production reference
53b7df96c91f9c12dcc8a07bcb9ccacbed38856a. -
docker/login-action v4.6.0
— registry authentication action, production reference
dbcb813823bdd20940b903addbd779551569679f.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.