How I set up GitLab runners for my projects
After configuring a few runners I realized: the question isn't "shell or docker", it's "which host does this runner live on, and what is it allowed to touch".
Which host to place them on
My current principle: a deploy runner lives on the same host it deploys to.
That enables DooD — the job container drives the host's docker
through a mounted /var/run/docker.sock, no SSH needed at all.
Runners for lint and tests can live on another continent — they don't need the host, just CPU and network.
Which executor to pick
Docker executor — 90% of cases
The job runs in a clean container from an image like image: node:22.
Nothing leaks between jobs, clean and predictable.
[[runners]]
name = "shared-check"
executor = "docker"
[runners.docker]
image = "alpine"
volumes = ["/cache"]
Docker executor with DooD — for deploys
Add the socket and, for a group runner, the releases directory:
[[runners]]
name = "deployer"
executor = "docker"
[runners.docker]
image = "docker:28"
user = "runner" # uid:gid — so the job isn't root on the socket
volumes = [
"/var/run/docker.sock:/var/run/docker.sock",
"/opt/apps:/mnt/deploy:ro", # shared releases dir for all projects
"/cache"
]
What else you'll need:
- a
runneruser in thedockergroup on the host (usermod -aG docker runner); $DEPLOY_DIRdirectories owned by that user — otherwisemkdirin the job fails with Permission denied (verified on my own rake);- for jobs that need docker-compose — an image with the CLI inside (I build my own
kovss-ci-deploy).
Shell executor — special cases only
The job runs directly on the host, no isolation. I almost never use it: it's dirty and jobs can see everything. The exception is building something that stubbornly refuses to fit into a container.
How I keep the cache bloat in check
Runners eat disk in three ways: GitLab cache (/cache or S3),
buildkit layer cache, and images themselves. My defense system:
- GitLab cache — S3 or with a limit. For local
/cacheon the host disk I set a timer:
with a daily timer (OnCalendar=daily, Persistent=true).# /etc/systemd/system/gitlab-cache-cleanup.service ExecStart=/bin/sh -c "find /cache -maxdepth 3 -type d -mtime +7 -exec rm -rf {} +" - Build cache — in the build job's after_script:
docker builder prune -af --filter until=72h. - Images —
docker image prune -fthere too, plus a weeklydocker system prune -afover ssh, when I'm looking at alerts anyway. - Disk alert at 80% — without it, any timer set eventually loses.
One more small thing that saved my nerves: name runner tags by meaning
(check, test, build, deploy) and bind jobs to tags in the pipeline —
then a deploy never accidentally goes to a runner without the socket,
and lint doesn't wait for the deploy machine to free up.