Chapter 05Lesson 02~155 minutes

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.

Hands-onShell vs DockerWorkspace evidenceKubernetes modelAutoscaling

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.
Mandatory lab path: no production runner, cloud account, or Kubernetes cluster is required. Use a disposable local machine/VM and Docker for direct observations; use documented configuration/state fixtures for executor types you cannot safely provision. Never weaken host security simply to make an executor demo run.

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.
Do not copy cloud credentials into course YAML. Provider credentials belong on the trusted runner-manager/provisioning side or an appropriate workload identity mechanism. The chapter’s mandatory path is architecture/evidence analysis, not provisioning paid cloud resources.

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.

Next lesson

Configuration, Design Choices, and Tradeoffs

Turn the observations into executor-selection criteria for trusted hardware jobs, general CI, cloud-native fleets, and elastic ephemeral capacity.

Knowledge check

Why does image: not convert a Shell runner into a Docker runner?

The Docker container was removed but a bind-mounted marker remains. Is that a cleanup contradiction?

A Kubernetes job pod disappears after failure. Which evidence should be preserved before retry?

Why model autoscalers without provisioning cloud infrastructure in the mandatory lab?

A job is pending before any worker exists. Is that necessarily an executor-image failure?

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.
Verification note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.