In GitLab Runner's config.toml, what does the `executor` setting control, and how do the shell, docker, and kubernetes executors differ?
answer
- the agent polls, the executor builds the environment
- host process vs container vs pod per job
- state persists on shell, nothing persists on kubernetes
- image: is meaningless without a container executor
basics
~20 sThe executor decides how GitLab Runner creates the environment a job's script runs in. Shell runs commands directly on the runner host, docker starts a fresh container per job from the job's image, and kubernetes schedules a pod per job in a cluster.
solid answer
~50 sGitLab Runner is only an agent: it polls GitLab for jobs and then hands each one to an **executor**, set per runner in `config.toml`. The `shell` executor runs the job's script as a child process on the runner host, so whatever is installed there is the build environment — fast, but state leaks between jobs and a job can read anything the runner user can. The `docker` executor starts a container from the job's `image:`, plus containers for any `services:`, and runs the script inside; each job gets a clean filesystem, and the runner's helper image handles clone, cache and artifacts. The `kubernetes` executor asks a cluster to schedule a **pod per job** — build, helper and service containers together — which gives the same isolation plus elastic capacity and no long-lived build hosts. Choice drives isolation, how caching works, and startup latency.
code
toml · 17 linesconcurrent = 8
[[runners]]
name = "linux-docker"
url = "https://gitlab.example.com/"
token = "glrt-EXAMPLE"
executor = "docker"
[runners.docker]
image = "alpine:3.20"
privileged = false
volumes = ["/cache"]
[[runners]]
name = "macos-shell"
url = "https://gitlab.example.com/"
token = "glrt-EXAMPLE2"
executor = "shell"go deeper
Know that the runner is an agent and the executor decides where the script actually runs: on the host, in a container, or in a cluster pod. Recall that image: needs a container executor.
Explain the per-job lifecycle of each executor, the role of the helper container, and why state persists on shell but never on kubernetes. Name the caching consequence of each.
Demonstrate the operational tradeoffs: warm image caches on fixed Docker runners versus elastic pods, resource requests causing Pending jobs, and which executors you would ever expose to fork merge requests.
Own the fleet strategy — which workloads get disposable environments, how build capacity is funded and autoscaled, and how you keep one blessed executor configuration consistent across many teams instead of per-team snowflakes.
## The runner and the executor are different things `gitlab-runner` is a small agent process. It authenticates to a GitLab instance, long-polls for jobs it is eligible to run, and then delegates the actual work. What it delegates *to* is the executor, chosen per runner entry in `config.toml`: ```toml concurrent = 8 [[runners]] name = "linux-docker" url = "https://gitlab.example.com/" executor = "docker" [runners.docker] image = "alpine:3.20" privileged = false ``` One runner process can hold several `[[runners]]` entries, each with a different executor. The YAML in `.gitlab-ci.yml` never picks the executor — it picks a runner via `tags:`, and the runner's configuration decides the rest. That indirection is why `image:` is silently ignored on a shell-executor runner: there is no container to apply it to. ## shell The script is written to a temporary file and executed by a shell on the runner host, as the user the runner service runs as. There is no isolation layer at all. - **Environment** = whatever is installed on the box. Adding a build dependency means changing the machine, not the pipeline. - **State persists.** The build directory, package caches, `~/.gradle`, Docker images pulled by earlier jobs — all still there next job. That is why shell runners feel fast and why they produce "works on runner 3, fails on runner 4" bugs. - **Trust.** Any job that lands here runs with the runner user's rights on a real host. For a public project or forks, that is an unacceptable blast radius. It earns its place for jobs that genuinely need the host: driving a hypervisor, using a hardware device, or macOS/iOS builds where containers are not an option. ## docker For each job the runner pulls the job's `image:` and starts a container for the build, plus one container per entry in `services:` (databases, `docker:dind`), on a shared network so the build can reach them by alias. A separate *helper* image performs the clone, cache extraction, artifact upload and Git submodule work, so those steps do not depend on tools existing inside your build image. - **Isolation** is the container boundary: a fresh root filesystem per job, and processes namespaced from the host. It is not a security sandbox against a hostile job — a job that gets `privileged = true` or the Docker socket effectively owns the host. - **Caching** must be explicit. Because the container is destroyed, anything you want back next time has to go through `cache:` or `artifacts:`; the cache itself lands on the runner host unless you configure distributed object storage. - **Cost** is the image pull and container start per job — seconds, mitigated by a warm local image store. ## kubernetes The runner talks to a cluster API and creates a **pod per job**. The pod holds the build container (your `image:`), the helper container, and a container per service. When the job finishes the pod is deleted. - **Elasticity.** Capacity is the cluster's problem; there are no build VMs to keep patched, and idle cost approaches zero if the cluster autoscales. - **Configuration surface is large.** Namespace, service account, resource requests and limits, node selectors and tolerations all live under `[runners.kubernetes]`, and getting requests wrong shows up as jobs stuck `Pending` while the cluster finds room. - **Ephemeral by construction.** Nothing survives between jobs, so caching depends entirely on object storage, and image pulls hit every new node unless you warm them. - **Building images is harder**, since there is no Docker daemon in the pod by default — which is the DinD-versus-daemonless discussion. ## Others worth naming `docker+machine` was the classic autoscaling executor (spin a cloud VM per job or per few jobs) and is deprecated in favour of the newer autoscaling executors that use a fleeting plugin; `ssh` and `custom` also exist, `custom` letting you script the whole lifecycle for exotic environments. ## How to choose Ask three questions: *how much do I trust the code that will run here* (fork MRs push you to disposable environments), *does the job need something only the host has*, and *how spiky is demand* (bursty CI favours per-job pods or autoscaled VMs; a steady trickle favours a couple of fixed Docker runners, whose warm image cache makes them cheaper and quicker).
- Why is `image:` in .gitlab-ci.yml ignored by some runners?Because `image:` only means something to an executor that creates a container — `docker`, `kubernetes` and their relatives. A shell-executor runner runs the script directly on the host, so there is nothing to apply the image to and the key is simply not honoured. If a job needs a specific toolchain image, it must be routed by `tags:` to a runner configured with a container executor.
- What does the helper image do in the docker and kubernetes executors?It runs the parts of the job that are not your script: cloning the repository, restoring and saving `cache:`, downloading and uploading `artifacts:`, and handling submodules. Keeping it in a separate container means your build image does not need Git or the runner's tooling installed, and it is why a minimal `image:` still gets a working checkout.
- When is the shell executor still the right answer?When the job genuinely needs the host: macOS or iOS builds, driving a hypervisor, using attached hardware, or a Windows toolchain that containers cannot serve. It is defensible for trusted, internal repositories where you control every commit — never for public projects or fork merge requests, since the job runs with the runner user's rights on a persistent machine.
saying these in an interview costs you the question
- Thinking the executor is chosen in .gitlab-ci.yml
- Claiming the docker executor sandboxes untrusted code securely
- Assuming caches persist automatically between container jobs
- Believing the kubernetes executor keeps one long-lived pod for all jobs
- Saying shell is just docker without containers, with no downside