Зачем вообще выбирать executor в GitLab Runner
GitLab Runner — агент, который забирает задания из GitLab CI/CD и исполняет их на вашей инфраструктуре. То, где и как выполняются команды job’а, определяет «исполнитель» (executor). Разберём два самых популярных варианта: docker executor и shell executor.
На практике выбор executor’а влияет на три вещи, которые обычно всплывают уже после первых инцидентов: воспроизводимость сборок, безопасность окружения и стоимость владения (сопровождение, чистка, отладка).
Если обобщать: Docker executor даёт изоляцию и предсказуемость окружения, Shell executor — минимальную прослойку и максимум прямого доступа к хосту. Но решают детали: как устроены cache, доступ к Docker, нужен ли privileged, какие права у файлов, где лежит workspace и как быстро накапливается «грязь» между сборками.
Модель исполнения: что реально происходит под капотом
Shell executor
Shell executor запускает команды job’а напрямую в ОС Runner’а (обычно через bash/sh или PowerShell). Всё выполняется на хосте: пакеты ставятся в систему, процессы живут в общей среде, а изменения окружения легко «перетекают» в следующие пайплайны.
Эффект: очень быстрый старт job’ов (нет pull образа и создания контейнера), но любая ошибка в pipeline может испортить хост или повлиять на соседние сборки.
Docker executor
Docker executor для каждого job’а поднимает контейнер на основе образа из image и запускает команды внутри него. Окружение должно быть описано образом (или этапами подготовки), поэтому сборки обычно более повторяемые: один и тот же образ — одни и те же версии инструментов.
Эффект: выше управляемость и изоляция, но добавляются накладные расходы на pull образов, тома, сеть, а также вопросы безопасности вокруг Docker daemon.
Ключевая разница такая: Shell executor доверяет job’у почти как процессу на сервере, а Docker executor пытается ограничить job контейнером. Но степень ограничения напрямую зависит от ваших настроек (особенно
privileged, volume mount’ов и доступа к Docker socket).
Если runner у вас живёт на отдельной машине, чаще всего это будет VDS: так проще разделять доступы, квоты и риски между продом и CI.

Безопасность: где чаще всего ошибаются
Shell executor: риск «исполняем код на сервере»
С Shell executor угрозная модель максимально прямолинейная: любой, кто может менять .gitlab-ci.yml (или пушить код в ветки, где запускается CI), потенциально запускает произвольные команды на хосте runner’а. Даже если runner работает под отдельным пользователем, остаются риски утечек и боковых эффектов.
- утечка секретов из файловой системы и переменных окружения;
- подмена бинарников в
PATHи «загрязнение» окружения; - доступ к локальной сети и ресурсам хоста (SSH-ключи, токены, конфиги);
- пересечение кешей/артефактов между проектами при слабой сегментации.
Shell executor уместен, когда проекты доверенные, хост выделен строго под CI, а доступы и сеть зажаты: отдельный пользователь, минимальные права, сегментация, разные runner’ы под разные уровни доверия.
Docker executor: изоляция не равна безопасности
Docker добавляет изоляцию процессов и файловой системы контейнера, но «безопасность по умолчанию» легко ломается настройками. Самые частые источники проблем:
privilegedрежим;- проброс сокета Docker (
/var/run/docker.sock) в контейнер; - слишком широкие volume mount’ы (например, чувствительные директории хоста);
- контейнеры без ограничений по capabilities/seccomp и без AppArmor/SELinux-политик.
Важно понимать: если контейнер получает доступ к Docker daemon хоста (через сокет) или запускается с privileged, для атакующего это часто эквивалент root-доступа к хосту. В таких конфигурациях тезис «docker executor безопаснее shell» превращается в иллюзию.
Про privileged: когда нужен и почему опасен
privileged в docker executor обычно включают ради Docker-in-Docker (DinD) или действий, требующих расширенных прав. Включение privileged резко расширяет возможности контейнера: управление устройствами, сетевыми настройками и другие «почти хостовые» права.
Практическое правило: включайте privileged только на отдельных runner’ах, только для конкретных проектов и только при понятной необходимости. Для тестов, линтеров и компиляции он почти никогда не нужен.
Воспроизводимость и дрейф окружения
Один из самых болезненных классов проблем в CI — «вчера собиралось, сегодня нет». Часто причина не в коде, а в изменившемся окружении.
Shell executor: дрейф неизбежен без дисциплины
Если вы ставите зависимости через apt/yum/pip/npm прямо на хост, версии со временем меняются. Даже «случайное» обновление пакетов на сервере может сломать пайплайны. Плюс остаются следы прошлых сборок: временные файлы, кеши, глобальные зависимости и остатки workdir.
Компенсация возможна, но требует дисциплины: иммутабельные образы VM, регулярная пересборка runner-хоста, запрет ручных установок, регламент уборки, разделение runner’ов по проектам.
Docker executor: окружение фиксируется образом
В docker executor вы фиксируете версии toolchain в Dockerfile/образе. Это делает pipeline повторяемым и переносимым: можно поднять такой же runner на другом хосте и получить то же поведение.
Рабочая практика: для каждого стека иметь базовый образ (node + конкретная версия менеджера пакетов, php + composer + расширения), обновлять его контролируемо и раскатывать изменения окружения как код.
Производительность: где быстрее и почему
«Быстрее» зависит от того, что вы делаете в job’ах и как устроен кеш.
- Shell executor чаще выигрывает по времени старта и по I/O на локальном диске, потому что нет контейнерной прослойки и оверхеда на pull/создание контейнера.
- Docker executor может быть очень быстрым на повторяющихся сборках при правильном использовании кешей зависимостей и локального кеша образов, но «холодный старт» (pull больших образов) иногда дорогой.
Типичные узкие места:
- скачивание зависимостей (npm/composer/pip/maven);
- сборка Docker-образов в CI;
- скорость диска и случайные IOPS (особенно для node_modules и vendor);
- параллельность: сколько job’ов одновременно на одном runner’е.
Если упираетесь в диск и кеширование зависимостей, полезно посмотреть и на серверный кеш: например, как организовать cache в Nginx через map и ключи (см. пример с map и кешем в Nginx).
Cache: как работает и почему «не ускоряется»
Слово cache в GitLab CI часто воспринимают как «волшебную папку». На деле кеш — это компромисс между скоростью, предсказуемостью и безопасностью.
Общий принцип cache в GitLab Runner
Кеш — набор директорий, которые runner пытается сохранить между job’ами. Важно помнить:
- кеш бывает локальным (на диске runner’а) или через удалённое хранилище (зависит от настройки runner’а);
- кеш легко «отравляется» после неудачной сборки, если ключи и пути выбраны неудачно;
- кеш не заменяет фиксацию версий зависимостей в lock-файлах.
Cache в shell executor
С shell executor кеш часто оказывается просто директорией на том же диске, где workspace. Это быстро, но рискованно: при обслуживании нескольких проектов кеши могут конфликтовать, а права файлов — ломаться.
Отдельная неприятность: «полу-кеш» может жить в workspace даже без механизма cache, и со временем становится непонятно, что именно ускоряет сборку и почему поведение на другом runner’е отличается.
Cache в docker executor
С docker executor кеш реализуется через примонтированные тома и/или механизм cache, который runner архивирует и восстанавливает. Тут часто всплывают права: контейнер пишет файлы одним UID/GID, а следующий job запускается под другим (зависит от образа). Итог — permission denied и «внезапные» падения.
Практика: кешировать только детерминированные директории менеджеров пакетов и привязывать ключи к lock-файлам, а не кешировать весь рабочий каталог.
Docker executor и сборка Docker-образов: три подхода
Один из главных аргументов в пользу docker executor — удобно собирать контейнеры в CI. Но тут и появляются самые «острые» настройки: privileged и доступ к Docker daemon.
1) Docker-out-of-Docker (проброс docker.sock)
Контейнер job’а получает доступ к Docker daemon хоста через /var/run/docker.sock. Плюсы: быстро, хорошо работает с кешем слоёв. Минусы: по сути, даёт job’у административный доступ к Docker на хосте, а значит часто и к самому хосту.
2) Docker-in-Docker (DinD) и privileged
Поднимается отдельный Docker daemon внутри контейнера. Часто требует privileged. Плюсы: лучше изоляция от Docker хоста (в теории). Минусы: сложнее, больше накладных расходов, много нюансов с сетью и кешем, и всё равно остаются риски из-за privileged.
3) Rootless/выделенная сборочная нода
Третий вариант — избегать privileged и сокета, используя rootless-сборку (если позволяет окружение) или выделенную сборочную ноду. Обычно это безопаснее, но требует более аккуратной инфраструктуры и регламентов.
Типовые сценарии: что выбирать в реальной жизни
Когда чаще выигрывает docker executor
- Несколько команд/проектов, нужна изоляция между ними.
- Нужно воспроизводимое окружение (одни и те же версии инструментов).
- Сложный toolchain (разные версии Node/Java/Python) и желание упаковать всё в образы.
- Важна «чистота»: каждый job стартует в контролируемой среде.
Когда уместнее shell executor
- Один доверенный проект и отдельный выделенный runner-хост.
- Сборки упираются в диск/IO, а контейнеризация добавляет накладные расходы.
- Нужна тесная интеграция с хостом: доступ к специфичному железу, системным сервисам, нестандартным сетевым настройкам.
- Хочется «минимум слоёв» для отладки: что видите в логе, то и на машине.

Чек-лист выбора: вопросы до внедрения
- Кто может менять pipeline? Если это не только админы — избегайте shell и особенно осторожно относитесь к docker.sock и
privileged. - Сколько проектов на одном runner? Чем больше мультиарендность, тем больше смысл в Docker executor и разнесении runner’ов.
- Нужно ли собирать Docker-образы? Если да — продумайте модель безопасности заранее.
- Где хранится cache? Локально или удалённо, какие ключи, что кешируем, кто может это читать.
- Как обновляете окружение? Образы (Docker) или управление конфигурацией хоста (Shell).
Практические грабли и как их обходить
Права файлов и «permission denied»
В docker executor разные образы могут работать под разными UID/GID. Если вы кешируете директории с результатами сборки, следующий job может не иметь прав их читать/перезаписать. Лечится унификацией пользователя в образах, аккуратным выбором кешируемых путей и разделением кешей по стеку/ветке/lock-файлу.
«Секреты утекли в кеш»
Плохая практика — кешировать домашний каталог целиком или директории, куда случайно попадают токены (конфиги CLI, файлы авторизации). Кеш должен содержать только то, что безопасно переносить между job’ами. Секреты — через защищённые переменные и scoped-токены, а не через файлы в кешах.
Непредсказуемость из-за смешивания окружений
С shell executor частая ситуация: «на этом runner’е когда-то ставили утилиту», поэтому сборка не падает, пока вы не перенесёте проект на другой runner. Если выбрали shell — документируйте зависимости хоста и автоматизируйте установку (конфигурация как код), иначе переносимость будет страдать.
Минимальные примеры конфигурации runner’ов
Ниже — ориентировочные фрагменты config.toml, чтобы визуально закрепить различия. Не копируйте без адаптации под вашу модель безопасности и изоляции.
Shell executor: базовая идея
[runners]
name = "shell-runner-01"
url = "GITLAB_URL"
token = "REDACTED"
executor = "shell"
Docker executor: базовая идея
[runners]
name = "docker-runner-01"
url = "GITLAB_URL"
token = "REDACTED"
executor = "docker"
[runners.docker]
image = "alpine:3.20"
pull_policy = "if-not-present"
privileged = false
disable_entrypoint_overwrite = false
oom_kill_disable = false
shm_size = 0
Итог: короткая рекомендация без фанатизма
Если важны воспроизводимость, изоляция и масштабирование на несколько проектов — чаще всего начинайте с docker executor, но держите под контролем privileged и любые «дырки» вроде проброса Docker socket.
Если у вас один доверенный проект и нужен максимум скорости/простоты, а хост выделен под CI и хорошо защищён — shell executor может быть оправдан. Но относитесь к этому как к запуску кода на сервере, а не как к «безопасному CI по умолчанию».
В большинстве инфраструктур лучший результат даёт комбинированный подход: отдельные runner’ы под разные классы задач и доверия, чёткая политика cache и запрет привилегий там, где они не нужны.


