Choosing a GitLab Runner Executor: Shell vs Docker vs Kubernetes
Shell, Docker, and Kubernetes executors each trade isolation for speed differently. Here's how to pick the right GitLab Runner executor for your workload, with config examples.
- #gitlab-cicd
- #ai
- #runners
- #executors
- #infrastructure
The single biggest lever on how your GitLab pipelines behave isn’t in .gitlab-ci.yml at all — it’s which runner executor you registered. The executor decides whether each job runs in a throwaway container, a shared shell, or a freshly scheduled Kubernetes pod. Get it wrong and you’ll fight flaky state, slow cold starts, or a security model that makes your platform team nervous. After running all three in production, here’s how I decide.
What the executor actually controls
When a runner picks up a job, the executor is the thing that creates the environment the job runs in, mounts the repo, runs your script, and tears it all down. The three you’ll realistically choose between are:
- Shell — runs jobs directly on the runner host, as a user on that machine.
- Docker — runs each job in a fresh container from your
image:. - Kubernetes — schedules each job as a new pod in a cluster.
Everything else — isolation, speed, cost, blast radius — flows from that choice.
Shell executor: fast, stateful, dangerous
The shell executor is the simplest. The runner runs your script lines as a shell on the host. No container pull, no scheduling, just go.
build:
stage: build
tags: [shell-runner]
script:
- make build
It’s genuinely the fastest option for cold starts because there’s nothing to provision. But the host is shared and persistent: leftover files, globally installed tools, and environment drift between jobs are all real. Worse, a job runs with whatever permissions that shell user has on a long-lived machine — so a malicious or buggy job can read other jobs’ leftovers or tamper with the host. I keep shell executors for tightly controlled, single-tenant cases: a dedicated release box, or a runner that only ever runs trusted internal jobs. For anything touching merge requests from contributors, it’s a non-starter.
Docker executor: the sensible default
For most teams, the Docker executor is the right answer. Each job gets a clean container from the image you specify, runs, and is destroyed. State doesn’t leak between jobs, and you control the toolchain per job via image:.
test:
stage: test
image: node:22-alpine
tags: [docker-runner]
script:
- npm ci
- npm test
The trade-offs are an image pull on cold caches and the classic Docker-in-Docker headache when a job needs to build images. You can mitigate the pull cost with a dependency proxy and a warm local image cache, and handle image builds with a services:-based DinD setup or a rootless builder. The isolation-per-job model is what makes Docker safe for mixed-trust workloads, which is exactly where shell falls down.
Kubernetes executor: scale and isolation, with overhead
The Kubernetes executor schedules each job as a pod. You get clean per-job isolation like Docker, plus elastic scale: the cluster spins pods up under load and reclaims them when idle. This is where you land when one runner host can’t keep up and you want capacity to track demand.
integration:
stage: test
image: python:3.12
tags: [k8s-runner]
script:
- pytest -q
The cost is operational complexity and per-job overhead — pod scheduling, image pull on cold nodes, and the need to tune pod resources so jobs neither get OOM-killed nor sit pending. If you’re already running Kubernetes, this executor pays for itself quickly; if you’re not, standing up a cluster just to run CI is a lot of yak-shaving. The deep tuning — resource requests, node selectors, tolerations — is its own topic worth a dedicated read.
A decision shortcut
Here’s the heuristic I actually use:
- Trusted, single-tenant, want minimum latency? Shell — but lock the host down and never point it at untrusted MRs.
- Mixed trust, moderate volume, want clean isolation without a cluster? Docker. This covers most teams.
- High volume, already on Kubernetes, need elastic scale? Kubernetes.
Let AI sanity-check your choice
This is a good place to use an LLM as a reviewer rather than an author. Describe your workload and constraints and have it pressure-test the trade-offs. I’ll often paste something like this:
Prompt: “We run ~400 CI jobs/day across 30 repos, a mix of internal and fork merge requests. Jobs are mostly Node and Python tests plus some Docker image builds. We do not currently run Kubernetes. Recommend a GitLab Runner executor, name the top two risks of that choice, and list what I’d need to change in .gitlab-ci.yml to support it.”
A useful answer comes back reasoned, not just asserted:
Output (excerpt): “Use the Docker executor. Fork MRs rule out the shell executor on isolation grounds, and adopting Kubernetes solely for CI adds operational overhead you don’t currently carry. Top risks: (1) image-build jobs need a DinD or rootless-builder pattern, which is a known footgun — isolate those to a dedicated runner; (2) cold image pulls add latency, so add a dependency proxy. In .gitlab-ci.yml, set a per-job
image:, tag image-build jobs to the DinD-capable runner, and…”
Treat that as a draft. The model doesn’t know your cluster doesn’t exist unless you tell it, and it can’t see your security boundaries — verify every claim against your actual constraints before you register anything. For the prompt patterns I lean on here, the runner config.toml executor tuning prompt and the broader GitLab CI/CD category are where I keep the reusable versions.
Mixing executors is fine — and often right
You don’t have to pick one. Register multiple runners with different executors and route jobs using tags:. A common shape: Docker runners for the bulk of test jobs, a locked-down shell runner for signed releases, and a Kubernetes pool that absorbs load spikes. The tag on each job decides where it lands, so you get the best executor per workload instead of one compromise for everything.
The bottom line
The executor is an architecture decision disguised as a registration flag. Shell trades isolation for raw speed and only belongs in trusted single-tenant corners. Docker gives you clean per-job isolation and is the right default for most teams. Kubernetes adds elastic scale at the price of operational overhead, and earns its keep when you’re already running a cluster and your volume demands it. Decide based on your trust boundaries and load first, then let the YAML follow — and use AI to challenge the choice, not to make it for you.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.