Runner Executors: Shell, Docker, Docker Autoscaler, Kubernetes, SSH, and Custom Execution Models: Guided Hands-On Workflow and Core Operations
Turn executor theory into observable evidence. You will compare a trusted shell-style execution boundary with a disposable Docker boundary, inspect workspace and process/container identity, model a Kubernetes pod lifecycle, trace Docker Autoscaler and Instance executor responsibilities, and document why SSH and Custom executors are exceptional rather than default choices.
Learning objectives
- Inspect the selected executor and worker/runtime identity without exposing runner authentication tokens or job secrets.
- Run or faithfully simulate the same trusted probe in a host-style Shell boundary and a Docker container boundary.
- Verify image, process, filesystem, network, volume, and teardown differences using concrete before/after observations.
- Map a Kubernetes executor job into manager → API request → per-job Pod → build/helper/service containers → cleanup state.
- Explain Docker Autoscaler and Instance executor worker lifecycles and why SSH/Custom executors require exceptional operational justification.
1. Scenario: one trusted probe, multiple execution boundaries
The synthetic job computes no proprietary data and requires no secret. Its purpose is to expose only safe execution metadata: source SHA, job/runner identity, hostname, user, OS, workspace, and selected filesystem markers. Comparing the same workload prevents application logic from obscuring executor differences.
Create a local directory such as
ch05-executor-lab outside valuable repositories. Every
local container name and volume uses the ch05- prefix
so cleanup can be exact rather than broad.
2. Preflight: record host and Docker state before mutation
LAB_ROOT="${TMPDIR:-/tmp}/ch05-executor-lab"
mkdir -p "$LAB_ROOT/evidence"
printf 'host=%s
' "$(hostname)" | tee "$LAB_ROOT/evidence/host-before.txt"
printf 'uid=%s gid=%s
' "$(id -u)" "$(id -g)" | tee -a "$LAB_ROOT/evidence/host-before.txt"
uname -a | tee -a "$LAB_ROOT/evidence/host-before.txt"
docker version --format 'client={{.Client.Version}} server={{.Server.Version}}' | tee "$LAB_ROOT/evidence/docker-version.txt"
docker ps -a --filter 'name=^/ch05-' | tee "$LAB_ROOT/evidence/docker-before.txt"
If any ch05- resources already belong to another
experiment, choose a different prefix. Do not use
docker system prune, docker volume prune,
or broad host cleanup commands.
3. Define one safe executor probe
#!/bin/sh
set -eu
printf 'probe=ch05-executor-boundary
'
printf 'host=%s
' "$(hostname)"
printf 'uid=%s gid=%s
' "$(id -u)" "$(id -g)"
printf 'pwd=%s
' "$(pwd)"
printf 'kernel=%s
' "$(uname -sr)"
if [ -r /etc/os-release ]; then
. /etc/os-release
printf 'os=%s version=%s
' "${ID:-unknown}" "${VERSION_ID:-unknown}"
fi
printf 'marker-created-by=%s
' "$(hostname)" > boundary-marker.txt
ls -ld . boundary-marker.txt
Save this as probe.sh and make it executable. The
script does not scan host credentials, network services, neighboring
workspaces, metadata endpoints, or privileged devices. Those would
be inappropriate “security demonstrations” on a machine you do not
fully own.
4. Observe a host-style Shell boundary safely
A real Shell executor would run the generated GitLab job script directly under the runner account on the runner host. For the mandatory free path, execute the probe in the disposable local directory under your current non-production user. This faithfully demonstrates host-process/workspace persistence without registering a runner.
cd "$LAB_ROOT"
cp /path/to/probe.sh ./probe.sh
chmod +x ./probe.sh
./probe.sh | tee evidence/shell-probe.txt
# The marker persists because the host directory persists.
test -f boundary-marker.txt
ls -l boundary-marker.txt | tee evidence/shell-marker-after.txt
Interpret the evidence: the hostname is the host, the process uses your host kernel and user identity, and the marker remains until you remove it. A production Shell runner has the same fundamental host-local boundary even though its user and workspace path differ.
5. Run the same probe in a disposable Docker boundary
Now run the exact script in an Alpine container with a bind-mounted
read-only input plus a dedicated writable output directory. Do not
use --privileged, host networking, host PID namespace,
the Docker socket, or sensitive host mounts.
mkdir -p "$LAB_ROOT/docker-output"
cp "$LAB_ROOT/probe.sh" "$LAB_ROOT/docker-output/probe.sh"
# Record image identity before execution.
docker image inspect alpine:3.22 --format 'id={{.Id}} repoDigests={{json .RepoDigests}}' | tee "$LAB_ROOT/evidence/docker-image.txt"
docker run --rm --name ch05-docker-probe --read-only --tmpfs /tmp:rw,noexec,nosuid,size=32m --mount type=bind,src="$LAB_ROOT/docker-output",dst=/work --workdir /work alpine:3.22 /bin/sh ./probe.sh | tee "$LAB_ROOT/evidence/docker-probe.txt"
docker ps -a --filter 'name=^/ch05-docker-probe$' | tee "$LAB_ROOT/evidence/docker-after.txt"
The container disappears because of --rm, but the
marker persists in the deliberately mounted output directory. This
distinction is essential: container teardown does not erase data
stored in a persistent bind mount, volume, cache, registry, artifact
store, or external service.
6. Compare evidence without overclaiming isolation
| Question | Host-style observation | Docker observation | Production meaning |
|---|---|---|---|
| Hostname | Host identity. | Container hostname/ID-like value. | Worker identity differs; preserve the one relevant to the executor. |
| Kernel | Host kernel. | Normally same host kernel. | Container userland isolation does not create a separate kernel. |
| User | Local/runner host account. | Container UID/GID from image/config. | UID 0 inside a container is not automatically host root, but privilege/mounts can change risk. |
| Workspace | Persistent host directory unless cleaned. | Container layer plus explicit mounts. | Persistence comes from executor/volume policy, not from “container” as a label. |
| Teardown | Process exits; host remains. | Container removed with --rm. |
Worker reuse/destruction is a separate lifecycle decision. |
| Reproducibility | Depends heavily on mutable host tools. | Image captures much userland state. | Pin image/tool versions/digests where reproducibility matters. |
7. Translate the comparison into GitLab executor configuration
In a real disposable GitLab project, two runners can advertise
unique tags such as ch05-shell and
ch05-docker. The job configuration asks for the
intended execution capability; it does not choose the executor
directly.
stages: [probe]
.executor_probe:
stage: probe
script:
- printf 'sha=%s pipeline=%s job=%s runner=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" "$CI_JOB_ID" "$CI_RUNNER_ID"
- printf 'runner=%s version=%s arch=%s\n' "$CI_RUNNER_DESCRIPTION" "$CI_RUNNER_VERSION" "$CI_RUNNER_EXECUTABLE_ARCH"
- printf 'host=%s uid=%s workspace=%s\n' "$(hostname)" "$(id -u)" "$CI_PROJECT_DIR"
probe_shell:
extends: .executor_probe
tags: [ch05-shell]
probe_docker:
extends: .executor_probe
tags: [ch05-docker]
image: alpine:3.22
Tags route jobs to runner configurations whose managers already have
executors configured. A job cannot safely transform an arbitrary
Shell runner into Docker by adding image:; Shell does
not implement the image keyword as Docker/Kubernetes
do.
8. Model Kubernetes execution from observable objects
If you already own an authorized disposable Kubernetes runner, inspect one job’s pod and events. Otherwise use this model and expected-state fixture instead of creating a cluster solely for the lesson.
# Expected conceptual evidence — replace placeholders only in an authorized lab.
executor: kubernetes
pipeline_id: <PIPELINE_ID>
job_id: <JOB_ID>
runner_id: <RUNNER_ID>
namespace: <RUNNER_NAMESPACE>
pod: <JOB_POD_NAME>
containers:
- build
- helper
- service-if-configured
lifecycle:
- prepare-pod
- clone-restore-download
- user-build
- cache-artifact-upload
- pod-cleanup
Production evidence should include runner-manager logs and Kubernetes events before the failed pod is deleted. A job trace alone may not reveal image-pull, scheduling, admission, quota, RBAC, or node-level failures.
9. Model Docker Autoscaler and Instance without a cloud bill
| Layer | Docker Autoscaler | Instance executor | Evidence/guard |
|---|---|---|---|
| Runner manager | Persistent manager process. | Persistent manager process. | Version, config, system ID, logs. |
| Provisioner | Fleeting plugin + autoscaler. | Fleeting plugin + autoscaler. | Plugin/provider version, dedicated resource, permissions. |
| Worker | Cloud instance hosting Docker job container(s). | Cloud instance used directly by job. | Instance ID/image, capacity/reuse count. |
| Job environment | Docker executor semantics. | Full instance/OS/device semantics. | Container ID/image vs host identity/toolchain. |
| High-isolation pattern |
capacity_per_instance=1,
max_use_count=1.
|
Single-tenant/one-use instance policy when required. | Bound max instances, destroy compromised workers. |
| Cost control | Idle count/time, max instances, concurrency. | Same class of autoscaling controls. | Budget/capacity alarms and orphan cleanup. |
10. Why SSH and Custom need an exception record
Before adopting SSH or Custom for a new workload, write an exception record answering: what requirement cannot be met by Docker, Docker Autoscaler, Instance, or Kubernetes; who owns patching and compatibility; how secrets are stored; how worker residue is erased; how failures are correlated; what happens on Runner upgrades; and how retirement is verified.
If those questions cannot be answered, the executor is not “simple”—its complexity has merely moved outside GitLab Runner.
11. Challenge: choose the failing layer
A job requests ch05-docker and remains pending. Do not
debug the image yet: no executor has accepted the job. Check
runner/tag/protection/scope availability first. If the job reaches
running and fails with “executable file not found”
before your script emits its first line, inspect image
entrypoint/shell compatibility and executor preparation. If your
test command runs and returns 1, move to the script/tool layer.
Knowledge check
Why does image: not convert a Shell runner into a
Docker runner?
The executor is configured on the runner manager. Shell does not implement job images; Docker/Kubernetes executors do.
The Docker container was removed but a bind-mounted marker remains. Is that a cleanup contradiction?
No. Container lifecycle and mounted persistent storage lifecycle are separate.
A Kubernetes job pod disappears after failure. Which evidence should be preserved before retry?
Pipeline/job IDs, runner-manager logs, pod identity/status, Kubernetes events, container logs, image identity, and relevant scheduling/RBAC/admission evidence.
Why model autoscalers without provisioning cloud infrastructure in the mandatory lab?
The executor-selection concepts can be learned safely/free while avoiding credentials, costs, orphan resources and uncontrolled cloud side effects.
A job is pending before any worker exists. Is that necessarily an executor-image failure?
No. Pending commonly points first to runner eligibility/capacity/routing. Executor image preparation begins only after an eligible runner accepts the job.
Version and compatibility note
GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.
Official references and version notes
- GitLab Runner executors — current executor selection guidance, compatibility matrix, actively developed paths, and maintenance-mode executors.
- Shell executor — host-local execution, shell selection, process termination, maintenance-mode status, and security warnings.
- Docker executor — image/service model, container lifecycle, volumes, image pull behavior, and executor configuration.
- Docker Autoscaler executor — Fleeting-based autoscaling, Docker feature compatibility, autoscaling resources, capacity and ephemeral-worker patterns.
- Instance executor — Fleeting-based instance provisioning, full host access, worker images, capacity, and native-step considerations.
- Kubernetes executor — per-job Pod creation, build/helper/service containers, RBAC requirements, entrypoint behavior, and executor configuration.
- SSH executor and Custom executor — exceptional/maintenance-mode execution models and operational constraints.
- Security for self-managed runners — Shell risk, Docker privilege/capability guidance, network segmentation, and trust-boundary recommendations.
- Use Docker to build Docker images — security and executor implications for Docker-in-Docker and socket/pipe binding.
Executor behavior and status were rechecked against current primary GitLab documentation on 2026-09-11. At verification time, Docker, Docker Autoscaler, Instance, and Kubernetes are actively developed executor paths; Shell, SSH, VirtualBox, Parallels, and Custom are maintenance mode, and Docker Machine is deprecated. Docker Autoscaler is documented as generally available since GitLab Runner 17.1 and uses Fleeting plugins. Re-check these facts before future course revisions.
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.