GitLab Runner with Docker Executor: Production CI/CD on Linux
Automation

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

bash
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
sudo systemctl enable --now docker
docker --version

Step 2: Install GitLab Runner

bash
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner
gitlab-runner --version

Step 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.

bash
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" \
[...]
Command truncated. Copy to view full command.

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.

bash
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"
[...]
Command truncated. Copy to view full command.

Step 5: Create Cache Directory

bash
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-pager

Step 6: Example .gitlab-ci.yml

Pin your runner with tags and use cache directives to speed up dependency installation across pipeline runs.

yaml
stages:
  - test
  - build
variables:
  DOCKER_DRIVER: overlay2
default:
  tags:
    - docker
[...]
Command truncated. Copy to view full command.

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=false to prevent accidental job pickup.
  • Store secrets in GitLab CI/CD variables (masked + protected), never in .gitlab-ci.yml.
  • Run periodic docker system prune via cron to reclaim disk space.
  • Consider separate runners for untrusted fork MRs vs internal branches.

Step 8: Disk Cleanup Cron

bash
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-cleanup

Monitoring and Troubleshooting

bash
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 group

Scaling 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

Technical Author - Liam K.
Liam K.

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