Runner Executors: Shell, Docker, Docker Autoscaler, Kubernetes, SSH, and Custom Execution Models: Concepts, Architecture, and Mental Model
A runner manager answers “which GitLab Runner process accepted this job?”; an executor answers “what execution environment did that manager create or select for the job?” This lesson separates those identities and compares Shell, Docker, Docker Autoscaler, Instance-era autoscaling, Kubernetes, SSH, and Custom models by isolation, lifecycle, reproducibility, elasticity, and operational ownership.
Learning objectives
- Explain the boundary from runner manager to executor provisioning, source/cache/artifact transfer, job process/container/pod/instance, and teardown or reuse.
- Distinguish runner configuration/manager identity from executor type, worker identity, image, filesystem, network, volumes, privileges, and lifecycle.
- Compare actively developed Docker, Docker Autoscaler, Instance, and Kubernetes executors with maintenance-mode Shell, SSH, and Custom paths using current GitLab guidance.
- Explain why Shell host reuse, Docker privileged mode, host Docker socket access, and broad Kubernetes permissions can materially change the trust boundary.
- Identify evidence that proves where a job actually ran and what isolation assumptions are justified versus merely hoped for.
1. Why executor choice is a security and operations decision
Two jobs can have identical .gitlab-ci.yml and still
have radically different risk. On a Shell executor,
repository-controlled commands execute directly on the runner host
under the runner account. On a Docker executor, commands normally
execute in a job container, but privileged mode or a mounted host
Docker socket can grant host-level control. On Kubernetes, each job
receives a pod, yet broad service-account permissions, host mounts,
privileged containers, or unsafe cluster co-tenancy can erase much
of the intended isolation.
The executor therefore answers more than “what command starts the job?” It defines provisioning, filesystem reuse, process namespace, image/runtime control, network reachability, volume exposure, teardown, observability, and which infrastructure must be trusted after running repository-controlled code.
2. Mental model: manager → executor → worker → teardown
flowchart TD
A[GitLab job is eligible] --> B[Runner manager accepts job]
B --> C[Configured executor]
C --> D[Prepare execution environment]
D --> E[Transfer source/cache/artifacts]
E --> F[Run job process, container, pod, or instance]
F --> G[Upload trace/reports/artifacts/cache]
G --> H[Teardown, reuse, or destroy worker]
H --> I[Manager becomes available for more work]
The key diagnostic question is: which layer owned the first unexpected state? A job that never matches a runner is a routing problem. A matched job whose container image cannot start is executor preparation. A running job whose test command exits 1 is script/tool behavior. A successful script followed by failed artifact upload belongs to post-build transfer—not the test itself.
3. Define the execution objects before comparing them
| Object | What it owns | Evidence to preserve |
|---|---|---|
| Runner manager | Long-lived GitLab Runner process/configuration that authenticates, polls, accepts jobs, and invokes an executor. | Runner ID, manager/system identity, version, tags/scope/protection, config location, logs. |
| Executor | Implementation that creates/selects the environment where a job executes. |
Executor name, relevant config.toml settings,
feature compatibility, privilege model.
|
| Worker | The concrete host/container/pod/instance used for one or more jobs. | Hostname/container ID/pod UID/instance ID, lifecycle, reuse count, image/OS. |
| Job image | Filesystem/userland used by container-capable executors. | Image reference/digest, entrypoint/shell availability, package/tool versions. |
| Service | Companion container/process such as a database for the job. | Image/digest, alias, ports, readiness evidence, logs. |
| Workspace | Checkout/build directory visible to the job. | Path, ownership, mount type, cleanup behavior, cross-job persistence risk. |
| Privilege boundary | Kernel/host/cluster permissions that the job can exercise. | UID/GID, capabilities, privileged flag, mounts, socket access, Kubernetes service account/RBAC. |
| Teardown policy | Whether the worker is cleaned, reused, or destroyed after the job. | Container/pod/instance deletion, persistent volume state, residue checks, reuse count. |
4. Current executor support is not symmetric
| Executor | Current lifecycle | Natural fit | Primary caution |
|---|---|---|---|
| Docker | Actively developed | Reproducible containerized jobs on Docker-capable hosts. | Container isolation is configurable, not absolute; privileged mode/socket mounts can collapse it. |
| Docker Autoscaler | Actively developed | Elastic Docker jobs backed by autoscaled instances through Fleeting. | Cloud/Fleeting complexity, capacity policy, dedicated autoscaling resources, cost bounds. |
| Instance | Actively developed | Jobs needing full instance/OS/device access with autoscaling. | Worker has host-level access; image hardening, tenancy and teardown become critical. |
| Kubernetes | Actively developed | Cloud-native fleets that create a pod per job in an existing cluster. | Cluster RBAC, host mounts, privileged pods, quotas/network policy and control-plane complexity. |
| Shell | Maintenance mode | Trusted workloads requiring direct host tools/devices with minimal executor machinery. | Limited isolation; arbitrary job code runs on the runner host as the runner user. |
| SSH | Maintenance mode | Exceptional remote-host execution where legacy constraints require it. | Least-supported feature set, remote host residue/credentials, weak reproducibility. |
| Custom | Maintenance mode | Existing bespoke provisioning systems with a maintained driver contract. | You own the driver, protocol, lifecycle, security, compatibility and failure semantics. |
5. Shell executor: the job is a host process
The Shell executor runs generated job scripts locally on the machine where GitLab Runner is installed. Dependencies must exist on that host, and builds run with the permissions of the runner user unless deliberately configured otherwise. This simplicity can be useful for a trusted hardware lab, legacy toolchain, or controlled workstation—but it creates a large trust boundary.
GitLab’s current security guidance explicitly treats Shell as high risk for untrusted builds because job code can inspect or alter host state and potentially steal code or credentials left by other projects. Even without root, a job can abuse whatever the runner account can access.
# Safe evidence to collect from a trusted Shell-executor job.
printf 'executor_model=shell
'
printf 'host=%s
' "$(hostname)"
printf 'uid=%s gid=%s
' "$(id -u)" "$(id -g)"
printf 'workspace=%s
' "$CI_PROJECT_DIR"
printf 'sha=%s job=%s runner=%s
' "$CI_COMMIT_SHA" "$CI_JOB_ID" "$CI_RUNNER_ID"
6. Docker executor: reproducible userland, configurable boundary
The Docker executor launches each job in a Docker image. A runner
configuration includes a default image, while a job-level
image: can override it. The executor also creates
helper/service containers as needed. This gives stronger environment
reproducibility than a mutable host because dependencies can be
encoded in an image reference and verified by digest.
But the container is not a magical hard security boundary.
privileged = true, host PID/network namespaces, broad
capabilities, sensitive host volumes, or direct access to the host
Docker socket can give the job extensive control over the runner
host. The secure question is not “is it Docker?” but “what host
resources and kernel authority does this container receive?”
docker_probe:
image: alpine:3.22
script:
- printf 'host=%s\n' "$(hostname)"
- printf 'uid=%s gid=%s\n' "$(id -u)" "$(id -g)"
- printf 'sha=%s job=%s runner=%s\n' "$CI_COMMIT_SHA" "$CI_JOB_ID" "$CI_RUNNER_ID"
- cat /etc/os-release | sed -n '1,4p'
For a reproducible production pipeline, replace a moving tag with an image digest after verifying the human-readable upstream release. Chapter 32 will revisit supply-chain pinning in depth.
7. Docker Autoscaler and Instance: Fleeting separates manager from elastic workers
Docker Autoscaler wraps Docker executor behavior but acquires worker instances on demand through GitLab Runner’s autoscaling stack and Fleeting provider plugins. It is documented as generally available since Runner 17.1. The Instance executor also uses Fleeting, but jobs run with full instance access rather than inside the Docker executor model.
The runner manager remains the long-lived scheduler-facing component; autoscaled instances are workers. This separation is valuable because a potentially compromised job worker can be destroyed while the manager remains outside the workload boundary. A common high-isolation pattern is one job per instance with bounded maximum capacity and destruction after use.
flowchart TD
G[GitLab] --> M[Runner manager]
M --> F[Fleeting plugin]
F --> C[Cloud autoscaling resource]
C --> W1[Ephemeral worker 1]
C --> W2[Ephemeral worker 2]
W1 --> J1[Job / container]
W2 --> J2[Job / container]
J1 --> X1[Destroy or reuse by policy]
J2 --> X2[Destroy or reuse by policy]
max_instances, idle capacity, reuse count, and cloud
permissions. This chapter models the architecture; it does not
require a paid cloud account.
8. Kubernetes executor: one job becomes a pod
The Kubernetes executor asks the Kubernetes API to create a pod for each GitLab CI job. The pod contains the build container plus GitLab helper behavior and any configured service containers. GitLab documents prepare, pre-build, build, and post-build phases: create the pod, clone/restore/download, execute user code, then upload cache/artifacts.
Per-job pods improve scheduling and resource control, but cluster isolation depends on namespace boundaries, service-account RBAC, pod security, hostPath mounts, privileged settings, node placement, network policy, and secrets. A pod that can create privileged pods or mount host files can become a cluster/host privilege problem.
flowchart TD
M[Runner manager] --> API[Kubernetes API]
API --> P[Per-job Pod]
P --> B[Build container]
P --> H[Helper container]
P --> S["Service container(s)"]
B --> L[Job script]
H --> A[Source / cache / artifacts]
P --> D[Pod deletion after job]
For production use, document the exact service account and permissions the runner needs, keep runner workloads separated from production workloads where appropriate, and retain manager/cluster events long enough to diagnose pods that disappear after failure.
9. SSH and Custom executors are exceptional paths
The SSH executor sends work to a configured remote machine. It is maintenance mode and among the least-supported executors. It can be necessary for a legacy appliance or fixed remote environment, but it inherits remote-host drift, credential management, residue, and weak portability.
The Custom executor lets operators integrate a bespoke provisioning/execution driver. That flexibility means your organization owns the complete lifecycle contract: prepare, run, cleanup, failure mapping, logging, compatibility, secret handling, and upgrade testing. Because Custom is also maintenance mode, a new platform should require a written reason why Docker, Instance, Docker Autoscaler, or Kubernetes cannot solve the need.
10. Evidence checklist: prove the boundary, not just the job status
Pipeline source, ref and immutable CI_COMMIT_SHA.
Runner ID/description, manager/system identity when available, Runner version.
Exact executor type plus relevant privilege/isolation configuration.
Host/container ID, pod name/UID, or instance ID and lifecycle/reuse policy.
Image release/digest or host image/OS and toolchain version.
Workspace path, mounts/volumes and expected persistence/cleanup.
Service aliases/ports and deliberate external reachability.
UID/GID, capabilities, privileged flag, socket/host mounts, service-account/RBAC where applicable.
Proof that disposable container/pod/instance resources were removed or intentionally retained.
Knowledge check
What is the difference between a runner manager and an executor?
The runner manager authenticates/polls/accepts jobs; the executor is the implementation that prepares or selects the environment in which an accepted job runs.
Why is Shell inappropriate for untrusted multi-project workloads?
Repository-controlled commands run directly on the runner host with the runner user’s permissions and limited isolation, so jobs can potentially access host or cross-project residue.
Does a Docker container automatically provide a hard security boundary?
No. Privileged mode, host namespaces/capabilities, sensitive mounts, or Docker-socket access can collapse much of the host boundary.
What does the Kubernetes executor normally create for each job?
A pod containing the build container plus helper behavior and configured service containers.
Why distinguish a Docker Autoscaler manager from its workers?
The manager is long-lived control infrastructure, while workers can be created/destroyed per capacity policy; evidence and compromise assumptions differ for each lifecycle.
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.