CoursesSecure CI/CD with GitLabGitLab CI fundamentals

Runners & executors

What actually runs your jobs.

Beginner12 min · lesson 2 of 17

GitLab decides what to run; a runner actually runs it. A runner is an agent you install that picks up jobs from GitLab, executes each in an environment chosen by its executor, and reports results back. The executor is the important choice: it determines the isolation each job gets, and therefore how contained a malicious build is. The docker and kubernetes executors — a fresh container or pod per job — are the secure default.

Runner executors, by isolation
isolated (prefer)
docker
fresh container per job
kubernetes
fresh pod per job
risky
shell
runs directly on the host — a bad build owns the runner
A per-job container/pod means one poisoned build is thrown away with its environment. The shell executor shares the host across jobs.

Shared vs project runners

Runners can be shared across the whole GitLab instance or dedicated to a project/group. Shared runners are convenient but mean many projects’ jobs run on the same machines, so a compromised job on a shared runner can affect others. For anything sensitive — especially jobs that touch production — use dedicated runners you control, and keep them separate from the runners open to forks and untrusted contributions.

config.toml (runner)
[[runners]]
name = "docker-runner"
executor = "docker"
[runners.docker]
image = "alpine:3.20"
privileged = false # do NOT enable unless you truly need DinD
volumes = ["/cache"] # never mount /var/run/docker.sock here

The privileged trap

To build container images inside a job you need a build engine, and the tempting shortcut is privileged: true (or mounting the host Docker socket) so the job can run Docker-in-Docker. Both hand the job root on the runner host — exactly the escape path the container courses warn about. Prefer a daemonless builder (Kaniko or BuildKit rootless) that builds images without a privileged runner at all.

Never run untrusted jobs on a privileged shared runner
A privileged runner, or one with the Docker socket mounted, gives every job it runs root on that host. Combine that with a shared runner open to forks and a merge request from anyone can own your CI infrastructure. Use isolated executors, avoid privileged mode, and keep production-capable runners off shared/fork pools.