Почти все причастные к разработке сталкивались с CI, и я уверен, что большинство знает про существование раннеров, на которых крутится наш CI. Но, как показывает практика, далеко не все знают о том, что раннеры бывают разных видов и что под разные сценарии DevOps в вашей команде подготовил разные окружения. Да и сами DevOps иногда оказываются в ступоре, когда на техническом интервью их спрашивают, чем docker от docker autoscaler отличается. Поэтому я решил собрать весь основной материал в виде большой шпаргалки. Сегодня разберём: какие раннеры бывают, какие плюсы и минусы у каждого, где какой лучше использовать, ну и на какие грабли вы можете наткнуться при работе.Данная статья написана для версии GitLab / GitLab Runner 18.x. GitLab постоянно подчищает хвосты в работе раннеров, поэтому будьте внимательны, на других версиях что-то может быть деприкейтед, а что-то просто не существует. Читать далее

Вечер DevOps инженера
Почти все причастные к разработке сталкивались с CI, и я уверен, что большинство знает про существование раннеров, на которых крутится наш CI. Но, как показывает практика, далеко не все знают о том, что раннеры бывают разных видов и что под разные сценарии DevOps в вашей команде подготовил разные окружения. Да и сами DevOps иногда оказываются в ступоре, когда на техническом интервью их спрашивают, чем docker от docker autoscaler отличается. Поэтому я решил собрать весь основной материал в виде большой шпаргалки. Сегодня разберём: какие раннеры бывают, какие плюсы и минусы у каждого, где какой лучше использовать, ну и на какие грабли вы можете наткнуться при работе.
Данная статья написана для версии GitLab / GitLab Runner 18.x. GitLab постоянно подчищает хвосты в работе раннеров, поэтому будьте внимательны, на других версиях что-то может быть деприкейтед, а что-то просто не существует.
На чём держится CI в GitLab?Сам GitLab — это большой босс, которому не до наших мелких задач по типу запуска пайплайнов, он занят хранением кода, парсингом .gitlab-ci.yml, созданием очередей и рисованием галочек и крестиков. Главное тут запомнить: GitLab сам ничего не делает. А вот настоящий трудяга тут — GitLab Runner — это просто один бинарь на Golang, который можно накатить куда душа пожелает: от кубера до мини-ПК у вас под столом.
Теперь предлагаю глянуть, как между собой общаются GitLab и Runner:
Регистрация — подписание оффера. Раннер с помощью токена представляется GitLab и дальше уже работает привязанным к инстансу, группе или проекту.
Пулинг — просим у босса новую задачу. Раннер — назойливый сотрудник, который долбит GitLab дефолтно раз в 3 секунды (check_interval) с вопросом «есть работа?». И самое главное, что соединение всегда исходящее от раннера, нам не нужно открывать порты или что-то подобное, наш сотрудник может также спокойно жить в закрытом контуре.
Джоба — получаем задачу от босса. GitLab даёт работу раннеру и вместе с ней одноразовый CI_JOB_TOKEN, с помощью которого раннер клонирует репу и забирает артефакты.
Исполнение — работник делает всё по ТЗ. Раннер выполняет скрипты через заданный executor и отдаёт обратно логи.
Чтобы сматчить джобу и раннер, GitLab использует теги:
build:
tags:
- docker
- linux
script:
- make build
Такая джоба прилетит раннеру, который содержит оба тега. Если джоба без тегов, то её получит раннер, которому разрешено запускать нетегированные джобы.
Ну и главное, ради чего мы тут собрались, по сути. Любой раннер, по сути, описывается двумя характеристиками:
Область видимости (проект, группа, инстанс)
Какая среда исполнения используется (shell, docker, kubernetes и т. п.)
Предлагаю разобрать обе по очереди. Начнём с той, что попроще.
Кому виден раннерInstance runnersЭти работяги видны всем проектам на конкретном инстансе. Создаются администратором. Очередь работает по принципу fair usage: джоба уходит на раннер с наименьшим числом уже работающих джоб, таким образом один жирный проект не съест все ресурсы, застопорив остальные.
Лучше всего подходит для однотипных задач: сборка образов, тестирование, шаблонный деплой. Отличная оптимизация относительно десятков простаивающих раннеров под каждый проект.
Group runnersЭти раннеры уже более выборочны, они принадлежат конкретной группе и всем её подгруппам. Создаются владельцем группы. Для очереди используется стандартный механизм FIFO (First-In-First-Out). Золотая середина, когда не хочется дёргать админа, чтобы он настраивал раннер, и при этом отдельный раннер на каждый проект кажется избыточным.
Project runnersСамые придирчивые работяги, готовы работать только с конкретными проектами. Создаются Maintainer. При этом уже существующий project-runner можно подключать к другим проектам, если при создании это не заблокировано. Также важно понимать, что форки не наследуют раннеры родителя в целях безопасности.
Эти раннеры идеальны для специфичных задач: деплой прода с кредами, GPU или нужна стабильность без вероятности, что кто-то ещё будет пользоваться этими же ресурсами.
GitLab-hosted runners: когда лень — это стратегияСвои раннеры — это, конечно, круто, но не все хотят этим заниматься, да и не каждому это нужно. Для этого GitLab.com предоставляет свои раннеры проектам, которые в нём живут. В результате каждая джоба получит свою эфемерную виртуалку, которая после завершения будет уничтожена. Эталонная изоляция, к которой должен стремиться self-hosted.
Однако бесплатный сыр только в мышеловке. Поэтому раннеры можно использовать не бесконечно. Мы платим за них compute minutes. Формула очень проста: минуты использования умножаем на cost factor раннера. После завершения потраченные compute minutes списываются из квоты namespace. Лимиты:
Free — 400 минут в месяц.
Premium — 10 000 минут в месяц.
Ultimate — 50 000 минут в месяц. При этом даже если мы держим проект на GitLab.com, свои раннеры не сжигают минуты, поэтому тяжёлые сборки лучше всего переносить на свои ресурсы.
GitLab предлагает несколько стандартных тегов:
Тег | vCPU | RAM | Диск | Тир | Cost factor |
|---|---|---|---|---|---|
| 2 | 8 GB | 30 GB | Free+ | 1 |
| 4 | 16 GB | 50 GB | Free+ | 2 |
| 8 | 32 GB | 100 GB | Premium/Ultimate | 3 |
| 16 | 64 GB | 200 GB | Premium/Ultimate | 6 |
| 32 | 128 GB | 200 GB | Premium/Ultimate | 12 |
| 2 | 8 GB | 30 GB | Free+ | 1 |
| 4 | 16 GB | 50 GB | Premium/Ultimate | 2 |
| 8 | 32 GB | 100 GB | Premium/Ultimate | 3 |
| 4 | 15 GB | 50 GB + Tesla T4 (16 GB) | Premium/Ultimate | 7 |
| 2 | 7.5 GB | 75 GB | Free+ | 1 |
| 4 | 8 GB | 50 GB | Premium/Ultimate | 6 |
| 6 | 16 GB | 50 GB | Premium/Ultimate | 12 |
Здесь есть теги на любой вкус — от GPU до macOS.
Executor — где крутится джобаExecutor — это среда, в которой будет отрабатывать наша джоба. Сейчас полный список выглядит так:
Executor | Изоляция | Автоскейлинг | Статус |
|---|---|---|---|
shell | нет | нет | maintenance |
docker | контейнер | нет | активно развивается |
docker autoscaler | контейнер на эфемерной ВМ | да | активно развивается |
instance | ВМ | да | активно развивается |
kubernetes | под | да, кластером | активно развивается |
virtualbox / parallels | ВМ | нет | maintenance |
ssh | нет | нет | maintenance |
custom | какую напишете | нет | maintenance |
Maintenance здесь указаны как в официальной документации. На них прилетают только фиксы по безопасности, нового функционала не планируется. Ну и король этого некролога — docker+machine — автоскейлер, который сам GitLab активно использовал, но с 17.5 признан deprecated, а в GitLab 20.0 будет полностью удалён.
Теперь пройдёмся по каждому.
shell: просто, но опасноСамый простой работяга. Скрипты работают прямо на машине, от лица пользователя gitlab-runner. Никакой изоляции или чистой среды.
Первые грабли — раннер не ставит зависимости, всё надо преднастраивать самому, нет make — джоба fail. Типовой пример использования:
deploy:
stage: deploy
tags: [deploy-runner]
script:
- systemctl --user restart my-app
environment: production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Здесь мы деплоим прямо на сервер, на котором стоит наш раннер. Сам GitLab официально заявляет, что такой вариант небезопасен, так как в ходе работы можно получить доступ к файлам хоста, к рабочим директориям других проектов, к секретам с этого же хоста. Также много мусорит, а за собой не убирает. А если вы ещё добавили пользователя gitlab-runner в группу docker, чтобы развернуть всё на хосте в контейнерах, то через docker-сокет джоба получает root.
Однако несмотря на всё это, сценарии использования есть, и они вполне имеют место быть: деплой на конкретный хост, тачка под конкретный проект.
Ну и предлагаю в этом и в последующих разделах взглянуть на создание самого раннера. За последние несколько лет этот процесс поменялся. Раньше (до 2023) был
registration token — общий секрет группы или проекта, который при создании раннера вписывался в его переменные. Теперь подход другой: прямо через UI GitLab мы создаём раннер, указываем теги и настройки и в результате получаем authentication token с префиксом glrt-. Этот токен также можно скормить нескольким тачкам, и каждая станет отдельной нодой одного логического раннера.
Процесс установки:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install gitlab-runner
В результате будет создан пользователь gitlab-runner и systemd-сервис. Дальше регистрируем раннер через
sudo gitlab-runner register
Для shell executor это будет выглядеть примерно так:
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.example.com/" \
--token "glrt-XXXX" \
--executor "shell" \
--description "shell-runner"
Ну и тривиальный config.toml:
concurrent = 1
[[runners]]
name = "deploy-runner"
url = "https://gitlab.example.com/"
token = "glrt-XXXX"
executor = "shell"
docker: хороший дефолтВ 90% случаев начинать лучше именно с него. Алгоритм работы прост: на каждую джобу создаём отдельный контейнер из образа, указанного в описании image:, при этом все вспомогательные задачи выполняет helper-container, он отвечает за clone, кэш и артефакты. Помимо основного контейнера, мы можем поднять дополнительные через директиву services:, это может быть какая-нибудь БД для тестов, например. После завершения джобы всё сносится, и следующая начинается с чистого листа. Также большой плюс — достаточно указать правильный image, который будет включать все нужные зависимости, таким образом получаем универсальный executor.
Примеры настройки:
concurrent = 4
[[runners]]
name = "docker-runner"
url = "https://gitlab.com/"
token = "glrt-XXXX"
executor = "docker"
[runners.docker]
image = "alpine:3.22" # дефолтный образ, если джоба не указала свой
privileged = false
pull_policy = "always"
volumes = ["/cache"]
Пример использования:
test:
tags: [docker-runner]
image: python:3.13-slim
services:
- postgres:17
variables:
POSTGRES_PASSWORD: test
script:
- pip install -r requirements.txt
- pytest
Ещё один важный момент — сборка docker-образов внутри docker executor. Тут у нас 2 пути:
docker-in-docker (dind). Здесь мы прописываем в config.toml строку privileged = true, тем самым мы отключили все механизмы безопасности, но при этом конкурентные сборки никак друг другу не мешают.
проброс docker.sock. Здесь мы не выдаём привилегии, но прописываем проброс: для этого мы маппим volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"] в config.toml. Таким образом, мы выдали полного рута для джобы, зато у нас общий кэш и без privileged. Также здесь есть конкуренция за имена среди всех контейнеров раннера. Ну и если прям паранойя по этому поводу и боишься, что кто-то пролезет в твой раннер, то rootless-сборщики вроде BuildKit приходят на помощь.
Тут с выбором всё очень просто: если у вас уже есть Kubernetes, то просто используйте его. На каждую джобу будет создаваться отдельный под по принципу docker executor: основной + helper + service, все три будут иметь общий localhost в рамках пода. Гибкость за счёт autoscaler самого кластера.
Ставится через Helm-чарт:
helm repo add gitlab https://charts.gitlab.io
kubectl create ns gitlab-runner
kubectl -n gitlab-runner create secret generic runner-token \
--from-literal=runner-token="glrt-XXXX"
helm install gitlab-runner gitlab/gitlab-runner -n gitlab-runner -f values.yaml
Минимальный рабочий values.yaml:
gitlabUrl: https://gitlab.example.com
runners:
secret: runner-token # тот самый secret с glrt-токеном
config: |
[[runners]]
[runners.kubernetes]
namespace = "gitlab-runner"
image = "alpine:3.22"
privileged = false
cpu_request = "500m"
memory_request = "512Mi"
cpu_limit = "2"
memory_limit = "4Gi"
concurrent: 10
checkInterval: 30
rbac:
create: true
Также хочу отметить, что лимиты на ресурсы можно передавать прямо из джобы:
build-fat-thing:
tags: [k8s]
variables:
KUBERNETES_CPU_REQUEST: "4"
KUBERNETES_MEMORY_REQUEST: "8Gi"
script:
- make build
Если у вас нет Kubernetes, то ради одного раннера это оверхед, но если уже используете, то отличное решение, но также будьте внимательны с dind.
docker autoscaler: облако под нагрузкуЕсли днём раннеры не успевают передохнуть от нагрузки, а ночью просто простаивают, то для этого есть docker autoscaler. Виртуалка сама будет создаваться в облаке, а при простое будет утилизироваться. Под капотом используется fleeting, плагинная абстракция над AWS ASG / GCP Instance Group / Azure VMSS. Официальная альтернатива великому docker+machine.
На AWS алгоритм будет следующий:
Создаём Auto Scaling Group с образом с предустановленным Docker и без своих политик скейлинга, так как это работа раннера
Ставим плагин: gitlab-runner fleeting install
Готовим конфиг
concurrent = 10
[[runners]]
name = "docker-autoscaler"
url = "https://gitlab.com"
token = "glrt-XXXX"
executor = "docker-autoscaler"
[runners.docker]
image = "alpine:3.22"
[runners.autoscaler]
plugin = "aws"
capacity_per_instance = 1 # джоб на одну ВМ
max_use_count = 1 # сколько джоб живёт ВМ; 1 = одноразовая
max_instances = 10
[runners.autoscaler.plugin_config]
name = "my-runner-asg" # имя ASG
[runners.autoscaler.connector_config]
username = "ec2-user"
[[runners.autoscaler.policy]]
idle_count = 2 # тёплый резерв
idle_time = "20m0s"
Главная формула: concurrent = max_instances × capacity_per_instance. Иначе мы получим либо простаивающие ВМ, либо очередь.
Здесь решена проблема dind, так как тачка всё равно умрёт после завершения, а значит, прятать просто нет смысла.
Из минусов: самая сложная настройка и холодный старт хоста, так как первая джоба будет ждать, пока облако поднимет тачку.
instance: автоскейлящийся shellDocker autoscaler, но без контейнеров, всё крутится прямо на ВМ. В конфиге просто заменяем executor на instance и убираем секцию [runners.docker]. Имеет место быть, например, когда собираем образы ОС или запускаем тесты, которые гоняют само железо. Ну и самое интересное — iOS-сборки на парке EC2 Mac.
Этих ребят мы встречаем всё реже, люди их почти добили.
virtualbox / parallels — раннер клонирует базовую ВМ из снапшота и по ssh подключается к ней. Можно использовать любую ОС плюс изоляция.
[[runners]]
executor = "virtualbox"
[runners.virtualbox]
base_name = "win2019" # имя базовой ВМ
base_snapshot = "clean" # снапшот, с которого клонируем
disable_snapshots = false
ssh — раннер по ssh ходит на хост и исполняет скрипты. Никакого кэша, а для артефактов на целевом хосте нужен обязательно gitlab-runner:
[[runners]]
executor = "ssh"
[runners.ssh]
host = "legacy.example.com"
port = "22"
user = "ci"
identity_file = "/etc/gitlab-runner/id_ed25519"
Имеет место быть только для хостов, на которые ну никак не поставить раннер, а деплоить надо прямо на этот хост (но есть же Ansible).
custom — конструктор для взрослых. Через скрипты жизненного цикла описываем, как будет работать раннер:
[[runners]]
executor = "custom"
builds_dir = "/builds"
cache_dir = "/cache"
[runners.custom]
prepare_exec = "/opt/ci-driver/prepare.sh" # например, lxc launch
run_exec = "/opt/ci-driver/run.sh" # lxc exec
cleanup_exec = "/opt/ci-driver/cleanup.sh" # lxc delete
Так, например, делают джобы в LXD или QEMU — всё, к чему сам GitLab не осмелился притронуться.
Выводы:В итоге мы получили отличные результаты:
hosted — когда лень, да и не особо надо, и да, это легитимная стратегия,
docker — дефолт для всего остального,
kubernetes — когда кластер уже есть,
docker autoscaler / instance — когда нагрузка скачет, а privileged-джобам нужны одноразовые машины,
shell/ssh — только деплой и только с пониманием рисков.
Ну а ребята по типу custom, virtualbox и parallels пусть останутся для полноты картины. Знать о них полезно, но применять маловероятно.
© 2026 ООО «МТ ФИНАНС»
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Почему ваш GitLab CI медленный: 6 ошибок в настройке Runner | 0 | 7 | 13-07-2026 |
| 2 | GitFlic CI/CD: первый конвейер с нуля — от коммита до деплоя | 0 | 7 | 07-07-2026 |
| 3 | Многоэтапные сборки в Docker: как уменьшить образ с 1,2 ГБ до 50 МБ | 5 | 8 | 28-06-2026 |
| 4 | Один файл, одна команда: как мы упростили запуск dev-окружения с помощью Runium | 0 | 12.11 | 24-07-2026 |
| 5 | Книга: «Основы DevOps и Software Delivery. Практика развертывания и сопровождения ПО в продакшене» | 0 | 6.5 | 07-07-2026 |
| 6 | С нуля до Junior DevOps в 2026 году. Часть 3. Git и GitHub | 0 | 6 | 12-07-2026 |
| 7 | Best CI/CD Concepts for DevOps | 5 | 7 | 06-03-2026 |
| 8 | The enterprise vibe coding problem, and what we built to solve it | 2 | 6 | 16-06-2026 |
| 9 | Мне надоело писать один и тот же код. Поэтому я сделал Featuregen | 3 | 6 | 09-07-2026 |
| 10 | Куда бежать с github? | -2 | 5 | 03-07-2026 |