Настройка self-hosted gitlab runner

Вторая часть материала о CI/CD в котором мы рассмотрим настройку self hosted gitlab-runner через docker.
О серии статей
Все найденные мной русскоязычные гайды не дают базового понимания того, как это работает, по большому счету это просто инструкции по настройке, причем под какой-то конкретный продукт и кейс: .net, Java, Node JS, etc.
Целью серии статей является детальное и схематичное описание того, как все это устроено. Главная задача — вооружить читателя фундаментальным пониманием, что конкретно ему требуется сделать в его конкретном случае. Помимо самой инструкции по настройке, это будет так же справочник для погружения в DevOps, охватывающий:
Максимально много функционала, которыми обладает Gitlab (общие абстракции актуальны и для его аналогов).
Инструменты которые нужны: Bash, Docker, Kubernetes и другие.
Общую теорию и практику с конкретными сценариями.
Материал разбит на 4 части.
Настройка GitLab CI/CD: понимаем принципы работы и запускаем первый pipeline
[Вы здесь → ] Настройка self-hosted gitlab runner (CI/CD)
[in progress 70%] Работа с registry (CI/CD)
[todo] Горизонтальное масштабирование CI/CD (высоко-нагруженный продакшн)
Оглавление
Настройка собственного gitlab runner
Запуск и проверка self hosted раннера
Если мы запускаем pipeline на gitlab.com (SaaS-инстанс), используются так называемые shared runners (общие для всех пользователей GitLab). Но не всегда хочется делить раннеры с остальными — иногда нужен свой. Для этого случая и написана статья.
Когда вам нужен свой gitlab runner?
Полный контроль и независимость — ваше железо, ваш раннер.
Доступ к внутренним ресурсам — развёртывание в приватную сеть, обращение к базам данных или API, недоступным извне
Контроль над железом — нужны GPU, много RAM или определённая архитектура (ARM, RISC-V)
Безопасность и комплаенс — код/артефакты не должны покидать инфраструктуру компании
Стоимость — большие объёмы CI/CD дешевле на своих мощностях, чем на SaaS-минутах GitLab
Производительность — устранение сетевых задержек, кеширование на быстрых локальных дисках
Настройка собственного gitlab runner
Есть множество способов установки gitlab runner, мы выберем вариант через docker container.
Подключаемся к серверу и создаем директорию inf-config — это будет рабочая директория в из которой мы будем запускать инфраструктурные сервисы:
mkdir inf-config
Создадим docker-compose файл с конфигурацией запуска нашего gitlab реннера:
touch docker-compose.yml
Содержимое:
services: gitlab-runner: image: gitlab/gitlab-runner:alpine3.21-v18.11.0 container_name: gitlab-runner restart: always volumes: - /srv/gitlab-runner/config:/etc/gitlab-runner - /var/run/docker.sock:/var/run/docker.sock
Что бы этот контейнер запустился, от имени администратора создаем директорию:
sudo mkdir -p /srv/gitlab-runner/config
В нем так же потребуется создать конфиг нашего ранера:
sudo touch config.toml
В gitlab нужно создать раннер для нашего репозитория, в нем мы возьмем токен.


Содержимое:
concurrent = 1 check_interval = 0 [[runners]] name = "laptop-dc" url = "https://gitlab.com/" token = "ВАШ_РАННЕР_ТОКЕН_ИЗ_НАСТРОЕК_GITLAB" executor = "docker" [runners.docker] image = "alpine:3.22.4" privileged = false disable_entrypoint_overwrite = false disable_cache = false volumes = ["/cache"]
Детальный разбор конфигурации
Документация по конфигурации gitlab runner в toml подробно объясняет все значения конфигурации, я кратко опишу только то, что использовал в нашем случае.
check_interval = 0 — Интервал времени в секундах для проверки раннером новых jobs. Иными словами как часто runner будет стучаться на gitlab с вопросом “есть ли для меня работа?”. Так как у нас 0, будет использовать значение по умолчанию (3 секунды).
concurrent = 1 — Лимит на кол-во параллельных jobs. Мы можем установить его внутри секции отдельного раннера что бы применить лимит на конкретный раннер. Так как мы указали значения в корне конфигурации, а не для отдельного раннера — мы установили глобальное ограничение для всех раннеров, на случай если их в будущем будет несколько, пока что у нас только 1.
url = "https://gitlab.com/" — Тут нужен URL Gitlab, если у вас self-hosted Gitlab, то нужен ваш адрес. Мы же используем Gitlab.com (SaaS-инстанс) по этому просто глобальный https://gitlab.com к которому наш раннер будет подключаться.
token = ВАШ_РАННЕР_ТОКЕН_ИЗ_НАСТРОЕК_GITLAB — сюда мы скопировали токен из настроек Gitlab (на последнем скриншоте действие 5).
executor = docker — Мы уже разобрали тему экзекьютеров выше, наш выбор Docker.
image = "alpine:3.22.4" — Образ по умолчанию который будет использован для всех джобов. Используем alpine — самая легковесная linux версия. На всякий случай конкретная версия, а не latest — так меньше вероятность что в какой-то момент что-то перестанет работать, в ранее работающих конфигурациях.
privileged = false — Это привилегированный режим запуска контейнеров. В обычном докере контейнер в привилегированном режиме запускается так: docker run --privileged alpine:3.22.4, в docker-compose.yml у сервиса ставиться флаг privileged: true. Что бы понимать все особенности этого режима, нужно хорошо ориентироваться в системе контроля доступов linux, об этом у меня есть статья на Хабре. Если совсем коротко, privileged:
Включает все capabilities (привилегии) ядра Linux — речь о механизме ограничения отдельных низкоуровневых операций процессов. Например, управление сетевыми настройками, но не уровне изменения какого нибудь
etc/network/interfaces, а напрямую на уровне операций, которые процесс может попросить выполнить ядро линукса (включить, отключить интерфейс, изменить MAC адресс и тд).Отключает стандартный профиль secure computing mode (seccomp) — механизм безопасности ядра, ограничивающий системные вызовы (syscalls). Примеры syscalls это: reboot, mount, open, read, write и т.д.
Отключает AppArmor.
Отключает метку процесса SELinux (Security-Enhanced Linux).
Предоставляет доступ ко всем устройствам хоста.
Делает виртуальную файловую систему (sysfs) доступной для чтения и записи.
Делает монтирования cgroups доступными для чтения и записи.
Думаю, сложилось понимание, что с привилегированным режимом нужно быть очень осторожным.
disable_entrypoint_overwrite = false — если здесь true, то нельзя переопределять entrypoint контейнеру из .gitlab-ci.yml. В shared runner стоит true, что ограничивает возможности в целях безопасности. Про entrypoint хорошо описано здесь. Кому интересно можете отследить появление этой фичи: MR c добавлением флага.
disable_cache = false — Данная опция сохраняет возможность использовать cache, который указывается в .gitlab-ci.yml. Этот кэш позволяет расшаривать данные между джобами. Например:
# Использование кэша в .gitlab-ci.yml cache: paths: - node_modules/ - .m2/repository/ - .pip-cache/
Кэш может работать через Docker volume или S3. Docker volume не имеет смысла в shared runners, так как там все джобы создаются в виртуалках и полностью изолированы друг от друга. В нашем случае — self-hosted runner, и мы можем использовать кэш для ускорения работы пайплайнов. Но стоит быть аккуратным и осознанно использовать кэш: без тонкого понимания жизненного цикла кэша можно создать трудноуловимые баги. Более совершенная реализация кэша — через S3. Подробнее: Advanced configuration/ the [runners.cache] section.
Запуск и проверка self hosted раннера
Необходимые конфигурации созданы:
/home/gtosss/inf-config/docker-compose.yml/srv/gitlab-runner/config/config.toml
Осталось только проверить, что все работает. В директории где лежит docker-compose.yml запускаем docker compose up. В моем случае рабочей директорией является /home/gtosss/inf-config или сокращенно ~/inf-config под пользователем gtosss. Я использую fedora и вместо docker у меня podman.
Переходим в рабочую директорию
cd ~/inf-config
Запускаем podman-compose up
gtosss@laptop-dc:~/inf-config$ podman-compose up [gitlab-runner] | Runtime platform arch=amd64 os=linux pid=2 revision=249f0215 version=18.11.0 [gitlab-runner] | Starting multi-runner from /etc/gitlab-runner/config.toml... builds=0 max_builds=0 [gitlab-runner] | Running in system-mode. [gitlab-runner] | [gitlab-runner] | FATAL: Service run failed error=stat /etc/gitlab-runner/config.toml: permission denied
Я погорячился создавая config.toml от имени супер пользователя. Причина ошибки в том, что podman в fedora работает в более безопасном режиме, чем docker в Ubuntu по дефолту. Podman разделяет управление контейнерами на отдельные режимы rootles и rootful, мы работаем от имени пользователя gtosss, podman поднимая контейнер назначает текущего пользователя его владельцем — это и есть режим rootles. Контейнеру нужен доступ к toml файлу, а мы его создали от имени рута.
Исправляем ситуацию руководствуясь подсказками из статьи — Права в Linux: chown/chmod, SELinux context, символьная/восьмеричная нотация, DAC/MAC/RBAC/ABAC. Если последующая информация вызывает вопросы, ответы будут как раз в этой статье.
Для начала посмотрим, какие права вообще назначены
gtosss@laptop-dc:~/inf-config$ stat /srv/gitlab-runner File: /srv/gitlab-runner Size: 12 Blocks: 0 IO Block: 4096 directory Device: 0,36 Inode: 5930632 Links: 1 Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) Context: unconfined_u:object_r:var_t:s0 Access: 2026-05-05 16:45:47.714091050 -0400 Modify: 2026-04-18 09:28:48.179756650 -0400 Change: 2026-04-18 09:28:48.179756650 -0400 Birth: 2026-04-18 09:28:36.917840577 -0400
Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) — владелец root.
Стоит обратить внимание на Context: Context: unconfined_u:object_r:var_t:s0
sudo chown -R gtosss:gtosss /srv/gitlab-runner
Пробуем запуститься:
gtosss@laptop-dc:~/inf-config$ podman-compose up [gitlab-runner] | cannot open `/run/user/1000/crun/78f1016f972c126ea6f02713931a3180dfee34b8ebaf40b2b8ba6e47c405939c/exec.fifo`: No such file or directory [gitlab-runner] | Error: unable to start container 78f1016f972c126ea6f02713931a3180dfee34b8ebaf40b2b8ba6e47c405939c: `/usr/bin/crun start 78f1016f972c126ea6f02713931a3180dfee34b8ebaf40b2b8ba6e47c405939c` failed: exit status 1
Решение нашлось в issues github (podman-compose репозиторий). Советуют просто compose down и затем compose up. Так себе решение, не особо понятна природа проблемы, но придется довольствоваться тем что есть.
Пояснение для тех, кого совсем уж не устраивает этот черный ящик.
/run/user/1000 — Это директория для временных файлов которые хранят информацию с момента загрузки системы (run-time variable data). После /user фигурирует UID (пользовательский ID), обычно это 1000. Почему именно тысяча обсуждалось например на linuxquestions.org в 2014. Есть конфигурация в /etc/login.defs, где среди прочих параметров указываются значения:
GID_MIN 1000 GID_MAX 6000
Проблема решилась, но мы опять сталкиваемся с ошибкой: error=stat /etc/gitlab-runner/config.toml: permission denied
В документации к podman о волумах говориться о метке z или Z. Ее нужно указывать в SELinux. То же самое и в документации docker. Согласно документации:
z(маленькая) - если volume разделяют несколько контейнеров (наш случай, т.к раннер будет создавать много контейнеров).Z(большая) - если volume использует только один контейнер.
Изменяем наш docker-compose.yml добавив метку z:
services: gitlab-runner: image: gitlab/gitlab-runner:alpine3.21-v18.11.0 container_name: gitlab-runner restart: always volumes: - /srv/gitlab-runner/config:/etc/gitlab-runner:z - /var/run/docker.sock:/var/run/docker.sock
Для docker.sock указывать не нужно, так как в нем уже настроен нужный SELinux контекст из коробки. Теперь все заработало. Кстати если сейчас посмотреть информацию о файле через stat:
gtosss@laptop-dc:~/inf-config$ stat /srv/gitlab-runner/config File: /srv/gitlab-runner/config Size: 56 Blocks: 0 IO Block: 4096 directory Device: 0,36 Inode: 5930634 Links: 1 Access: (0755/drwxr-xr-x) Uid: ( 1000/ gtosss) Gid: ( 1000/ gtosss) Context: system_u:object_r:container_file_t:s0
То можно заметить, что после запуска docker-compose up Context сам поменялся: unconfined_u:object_r:var_t:s0 → system_u:object_r:container_file_t:s0.
После запуска контейнера с gitlab-runner переходим в gitlab и смотрим есть ли соединение:


Спасибо, что дочитали!
Кто уже подписан на телеграм-канал — отдельная благодарность. Помимо технических статей поднимаем и социально важные темы.
Если материал полезен и изложен понятным языком — не стесняйтесь ставить плюсы. Это поможет выделиться на фоне остальных статей, так же это хороший способ поддержать меня.
С вами был Тимофей. Кто я?
Разрабатываю с 2015 года. Стартовал как front-end разработчик на React, после 6-лет переключился на full-stack, последние годы — чаще DevOps. Мой публичный WakaTime.
