Containers, Reproducible Runtimes, and Distributed Test Environments: Guided Hands-On Workflow
Build and inspect a version-pinned Robot Framework image, compare host and container execution, preserve results through an explicit Docker volume, and make runtime ownership observable.
Learning objectives
- Create a disposable host/container project with exact Python and Robot dependency assumptions.
- Build a non-root Robot image without baking runtime results or secrets.
- Run the same deterministic suite on the host and in a container and compare semantic results rather than timestamps/paths.
- Persist container result files through an explicit Docker named volume and export them before cleanup.
- Inspect image ID, configured user, mount destination, container exit code and Robot output as independent evidence.
Current compatibility baseline — verified 2026-08-31.
Robot Framework 7.4.2 is the stable course baseline
and requires Python 3.8+. The mandatory container lab uses the
Docker Official Image python:3.12.14-slim-bookworm and
installs the Robot Framework 7.4.2 universal wheel by exact version
and SHA-256 hash. Docker image tags are mutable, so the lesson
records the resolved image digest and explains full digest pinning
for controlled pipelines. Pabot 5.2.2 is discussed
as the current stable optional parallel executor; 5.3.0b1 is
prerelease and is not required. No browser library, external
API/database/SSH service, CI provider, Kubernetes cluster, paid
platform, real credential, or production target is required.
1. Disposable scenario and ownership contract
Create rf26-container-lab in a scratch location. The
suite is intentionally non-browser and non-networked: it proves
arithmetic plus creation of a small evidence file beneath Robot's
own output directory. That keeps the first container lesson focused
on runtime boundaries rather than on Selenium, Browser,
RequestsLibrary, databases or SSH.
rf26-container-lab/
├── Dockerfile
├── .dockerignore
├── requirements.txt
├── tests/
│ └── runtime_contract.robot
├── tools/
│ └── compare_results.py
├── results-host/
├── results-container/
└── evidence/
Source files are owned by the learner. The image owns its baked copy
of tests/. The container owns only its transient
process/writable layer. A named Docker volume owns the first
container result set until it is exported. Host directories
results-host and results-container own the
retained comparison artifacts.
2. Preflight: prove tools and empty evidence locations before building
python --version
python -m robot --version
docker version
docker info
git status --short 2>/dev/null || true
# Bash/zsh
mkdir -p results-host results-container evidence
find results-host results-container -maxdepth 1 -type f -print
# PowerShell equivalents:
# New-Item -ItemType Directory -Force results-host,results-container,evidence | Out-Null
# Get-ChildItem results-host,results-container
If python -m robot --version does not report 7.4.2, use
an isolated virtual environment and install the pinned version
before the host comparison. Record Docker client/server versions
because Docker Desktop and a remote Engine are not the same runtime.
3. Pin the Python package, including its wheel hash
robotframework==7.4.2 \
--hash=sha256:6e80f84cdc997bdde2abb6b729ac3531457ecf6d2e41abfb87a541877ab367bf
The hash is the SHA-256 of the Robot Framework 7.4.2 universal wheel
published on PyPI. --require-hashes makes pip reject a
different distribution. This is stronger evidence than a version
string alone.
4. Create a deterministic Robot suite whose mutable file lives under OUTPUT DIR
*** Settings ***
Library OperatingSystem
*** Variables ***
${RUNTIME_LABEL} unset
*** Test Cases ***
Arithmetic Contract
${answer}= Evaluate 6 * 7
Should Be Equal As Integers ${answer} 42
Log Runtime label: ${RUNTIME_LABEL}
Result Directory Contract
${proof}= Join Path ${OUTPUT DIR} proof.txt
Create File ${proof} rf26-container-contract
File Should Exist ${proof}
${content}= Get File ${proof}
Should Be Equal ${content} rf26-container-contract
Log Output directory: ${OUTPUT DIR}
The suite never writes beside its source file.
${OUTPUT DIR} is chosen by the runner, so the same test
can run under a host result directory or a mounted container result
directory. The runtime label is only observational metadata; the
assertions remain identical.
5. Keep build context narrow
.git
.venv
__pycache__/
*.pyc
results-host/
results-container/
evidence/
This prevents old logs, virtual environments and evidence from
becoming image layers. Never put credentials into the context merely
because .dockerignore currently excludes them; build
contexts and layer history should be treated as potentially
inspectable.
6. Build a version-pinned, non-root Robot image
# syntax=docker/dockerfile:1
FROM python:3.12.14-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
COPY requirements.txt /tmp/requirements.txt
RUN python -m pip install --no-cache-dir --only-binary=:all: --require-hashes \
-r /tmp/requirements.txt
WORKDIR /workspace
COPY --chown=10001:10001 tests/ /workspace/tests/
RUN mkdir -p /workspace/results && chown -R 10001:10001 /workspace
USER 10001:10001
ENTRYPOINT ["python", "-m", "robot"]
CMD ["--outputdir", "/workspace/results", "/workspace/tests"]
The numeric user keeps the image independent of a particular host
username. There is no need for root during execution. The
ENTRYPOINT preserves Robot's exit code as the container
exit code. Tests are baked deliberately in this first run; a
read-only bind-mount alternative is introduced later as a design
choice.
The Python tag is exact at patch/distribution level, but the tag can
still be republished. Record its resolved digest now; for controlled
long-lived pipelines, update FROM to include the full
manifest digest.
7. Record base provenance, then build and inspect
docker pull python:3.12.14-slim-bookworm
docker image inspect python:3.12.14-slim-bookworm --format '{{json .RepoDigests}}' > evidence/base-image-digests.json
docker build --tag rf26-robot:7.4.2 .
docker image inspect rf26-robot:7.4.2 --format '{{.Id}}' > evidence/test-image-id.txt
docker image inspect rf26-robot:7.4.2 --format '{{.Config.User}}' > evidence/test-image-user.txt
docker run --rm --entrypoint sh rf26-robot:7.4.2 -c 'id; python --version; python -m robot --version' > evidence/container-preflight.txt
Expected evidence: the configured user is 10001:10001;
Python reports 3.12.14; Robot reports 7.4.2. The image ID identifies
your built image, while the base-image digest records what the
upstream tag resolved to at build time.
8. Run the host baseline first
python -m robot --variable RUNTIME_LABEL:host --outputdir results-host tests/runtime_contract.robot
echo "host_exit=$?" > evidence/host-exit.txt
PowerShell: run the same
python -m robot command on one line (or use backticks),
then record $LASTEXITCODE. Expected artifacts are
results-host/output.xml, log.html,
report.html and proof.txt.
9. Run the image with an explicit result volume and preserve the stopped container until evidence is copied
VOL=rf26-results
NAME=rf26-run
docker volume create "$VOL"
docker create --name "$NAME" --mount type=volume,src="$VOL",dst=/workspace/results rf26-robot:7.4.2 --variable RUNTIME_LABEL:container --outputdir /workspace/results /workspace/tests/runtime_contract.robot
docker start --attach "$NAME"
docker inspect "$NAME" --format '{{.State.ExitCode}}' > evidence/container-exit.txt
docker inspect "$NAME" --format '{{json .Mounts}}' > evidence/container-mounts.json
docker cp "$NAME":/workspace/results/. results-container/
The stopped container is intentionally retained until the copy
succeeds. That is evidence-preserving behavior: do not use
--rm until you already know the output path is durable.
The named volume is the runtime result store;
docker cp creates the host copy used by the comparison
and later archival.
10. Compare semantic results, not byte-for-byte XML
from pathlib import Path
from xml.etree import ElementTree as ET
def status_map(path: str) -> dict[str, str]:
root = ET.parse(path).getroot()
result = {}
for test in root.iter("test"):
status = test.find("status")
result[test.attrib["name"]] = status.attrib["status"]
return result
host = status_map("results-host/output.xml")
container = status_map("results-container/output.xml")
print("host:", host)
print("container:", container)
if host != container:
raise SystemExit("Host/container semantic result mismatch")
for path in [
Path("results-host/proof.txt"),
Path("results-container/proof.txt"),
]:
assert path.read_text(encoding="utf-8") == "rf26-container-contract"
print("semantic outcomes and proof artifacts match")
python tools/compare_results.py
Robot output contains timestamps, absolute paths and runtime metadata, so byte identity is not a useful equivalence test. The comparison deliberately checks stable semantics: test names, PASS/FAIL states and domain proof content.
11. Optional comparison: mount tests read-only instead of baking them
# POSIX example; source is read-only. Result ownership may require matching host UID/GID.
docker run --rm --user "$(id -u):$(id -g)" --mount type=bind,src="$PWD/tests",dst=/workspace/tests,readonly --mount type=bind,src="$PWD/results-container",dst=/workspace/results rf26-robot:7.4.2 --variable RUNTIME_LABEL:mounted-source --outputdir /workspace/results /workspace/tests/runtime_contract.robot
This mode gives rapid source iteration, but the image no longer
contains the exact test source by itself. On native Linux the
explicit host UID/GID prevents root-owned or inaccessible result
files. Docker Desktop has different host-filesystem mediation, so
inspect ownership rather than copying a Linux fix blindly. Avoid
chmod 777.
12. Challenge: choose the correct layer
Your container run passes but the CI job cannot find
output.xml. Which layer should you inspect first?
Start with the runtime mount and Robot
--outputdir contract, not the test assertions. Prove
the container destination, mount destination and stopped-container
filesystem before rerunning. If results exist only inside the
container, copy them out before cleanup and fix the mount path for
the next run.
Knowledge check
Why does the lab retain a named container instead of using
--rm immediately?
It allows inspection and recovery of result files even if the volume/mount contract was wrong. Evidence is copied before destructive cleanup.
Why is the tests directory baked into the first image run?
It makes source provenance simple for the first comparison. A later read-only bind mount demonstrates the different trade-off without mixing both models at once.
What does USER 10001:10001 change?
It changes the OS identity of the container process and therefore filesystem permissions. It does not change Robot variable scope, library scope, or external-system authorization.
Why compare test statuses instead of comparing output.xml bytes?
Timestamps, absolute paths, generator metadata and other runtime details can legitimately differ. Semantic equivalence focuses on the stable test names/statuses and domain evidence.
13. Cleanup/rollback
docker rm rf26-run
docker volume rm rf26-results
# Keep results-host/, results-container/, and evidence/ if they are your evidence packet.
# Remove the disposable image only after you no longer need provenance inspection:
docker image rm rf26-robot:7.4.2
Do not delete the host results/evidence until the comparison and any review are complete. Cleanup removes only resources created by this lab; it never prunes unrelated images, volumes or containers.
14. Summary and bridge
You now have a reproducible execution chain with a pinned Python/Robot dependency set, explicit non-root user, image provenance, a durable result volume, host/container semantic comparison and evidence-first cleanup. Lesson 3 turns these mechanics into architectural choices for real teams.
References and version anchors
- Robot Framework 7.4.2 PyPI — version, Python requirement and wheel hash provenance
- Python Docker Official Image — Python 3.12.14 slim-bookworm tag availability
- Docker build best practices — pinning, build context, ephemeral containers and USER
- Docker container run reference — mount and read-only runtime controls
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.