GitLab Runner with Docker Executor: Production CI/CD on Linux
- Author :Liam K.
- Date :July 01, 2026
- Time :27 minutes
Self-hosted GitLab Runners give you full control over build environments, hardware, and costs. The Docker executor is the most popular choice because each CI job runs in an isolated container with a clean filesystem — no leftover artifacts from previous builds polluting your pipeline. This guide walks through a production-ready setup on a dedicated Linux server.
Whether you use GitLab.com with self-hosted runners or a self-managed GitLab instance, the runner configuration patterns are the same. Focus on isolation, cache efficiency, resource limits, and secrets hygiene — these four areas determine whether CI/CD helps or hurts your delivery velocity.
Architecture Overview
GitLab sends job definitions to registered runners. The runner manager pulls the job, spawns a Docker container with the specified image, executes the script stages, reports results, and destroys the container. Build caches and artifacts persist on the host via mounted volumes.
- Runner manager — long-lived process that polls GitLab for jobs.
- Job containers — ephemeral, one per job, destroyed after completion.
- Cache volume — shared directory for dependency caches across jobs.
- Docker socket — required for Docker executor; treat as high-privilege access.
Prerequisites
- Ubuntu 22.04+ or Debian 12 with 4+ CPU cores and 8+ GB RAM
- Docker Engine installed and running
- GitLab project or group with maintainer access to register runners
- Fast SSD storage — CI workloads are I/O intensive
Step 1: Install Docker
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
sudo systemctl enable --now docker
docker --versionStep 2: Install GitLab Runner
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner
gitlab-runner --versionStep 3: Register the Runner
Obtain your registration token from GitLab under Settings → CI/CD → Runners. Use a descriptive tag like docker,linux,production so jobs can target this runner explicitly.
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "YOUR_REGISTRATION_TOKEN" \
--executor "docker" \
--docker-image "docker:27" \
--description "prod-docker-runner-01" \
--tag-list "docker,linux,production" \
[...]Step 4: Configure Production config.toml
The default configuration is minimal. Extend it with concurrency limits, pull policies, cache directories, and resource constraints to prevent a single heavy job from starving the host.
sudo tee /etc/gitlab-runner/config.toml >/dev/null <<'EOF'
concurrent = 4
check_interval = 3
log_level = "info"
[[runners]]
name = "prod-docker-runner-01"
url = "https://gitlab.com/"
token = "RUNNER_TOKEN_FROM_REGISTER"
[...]Step 5: Create Cache Directory
sudo mkdir -p /cache/gitlab-runner
sudo chown -R gitlab-runner:gitlab-runner /cache
sudo systemctl restart gitlab-runner
sudo systemctl status gitlab-runner --no-pagerStep 6: Example .gitlab-ci.yml
Pin your runner with tags and use cache directives to speed up dependency installation across pipeline runs.
stages:
- test
- build
variables:
DOCKER_DRIVER: overlay2
default:
tags:
- docker
[...]Step 7: Security Hardening
Runners with Docker socket access can effectively root the host. Restrict which projects and branches can use the runner, and never enable privileged = true unless a specific job requires it (e.g. Docker-in-Docker builds).
- Lock runners to protected branches only in GitLab settings.
- Use
run-untagged=falseto prevent accidental job pickup. - Store secrets in GitLab CI/CD variables (masked + protected), never in
.gitlab-ci.yml. - Run periodic
docker system prunevia cron to reclaim disk space. - Consider separate runners for untrusted fork MRs vs internal branches.
Step 8: Disk Cleanup Cron
sudo tee /etc/cron.daily/gitlab-runner-cleanup >/dev/null <<'EOF'
#!/bin/bash
docker container prune -f --filter "until=24h"
docker image prune -f --filter "until=72h"
docker volume prune -f --filter "label!=keep"
EOF
sudo chmod +x /etc/cron.daily/gitlab-runner-cleanupMonitoring and Troubleshooting
sudo gitlab-runner verify
sudo journalctl -u gitlab-runner -f
# Common issues:
# "no space left on device" → prune Docker images/volumes
# Job stuck in "pending" → check runner tags match .gitlab-ci.yml
# Permission denied on docker.sock → add gitlab-runner user to docker groupScaling Beyond One Runner
When queue times grow, add more runner hosts rather than increasing concurrent on a single machine beyond its CPU and memory capacity. Use the same tag set across runners for horizontal scaling. For bursty workloads, consider autoscaling runners on cloud instances that register on boot and deregister on shutdown.
"A production CI runner is a build factory, not a dev sandbox — isolate jobs, cap resources, and prune aggressively before disk becomes your bottleneck."
Technical Author

System administrator and technical writer specializing in server infrastructure, security and deployment. Creating comprehensive guides to help you master server administration.