GitLab Runner и Docker на VPS: self-hosted CI без переплаты за минуты
Shared runners GitLab.com удобны, пока пайплайны короткие и редкие. Когда сборка npm/Maven занимает 12–20 минут, в репозитории десяток MR в день, а ещё крутятся nightly и e2e — минуты SaaS заканчиваются быстрее, чем кажется. Свой GitLab Runner на VPS с Docker executor даёт предсказуемую стоимость, полный контроль над образами и кэшем и независимость от очереди чужих джоб.
Ниже — практический разбор: когда свой раннер выгоднее, какой VPS взять под сборки, как поставить runner и Docker на Ubuntu, зарегистрировать теги, не прострелить себе ногу privileged-режимом, не забить диск кэшем и артефактами, следить за очередью и сравнить смету с минутами GitLab.com. Базовую безопасность Ubuntu, firewall и fail2ban здесь не дублируем — на них есть отдельные гайды; ссылки в конце соответствующих разделов.
SaaS минуты GitLab.com или свой раннер
На GitLab.com бесплатный и платные тарифы ограничивают compute minutes на shared runners. Лимит общий на группу/аккаунт: CI для pet-проекта, staging-деплой и тяжёлый monorepo делят одну корзину. Когда минуты кончились, пайплайны встают до конца месяца или до доплаты.
Типичные триггеры «пора на свой runner»:
- сборка фронта или бэка стабильно 10+ минут, а MR много;
- нужны кастомные образы, GPU, большие volume, приватный registry без лишних слёз;
- хочется фиксировать версию Node/JDK/PHP на годы, а не ждать обновления shared-образа;
- compliance: код и артефакты не должны уезжать на чужие воркеры;
- ночные e2e и load-тесты съедают квоту быстрее дневной разработки.
Self-hosted runner не отменяет GitLab.com как Git/MR/Issues: вы по-прежнему пушите в gitlab.com (или в свой GitLab CE/EE), а джобы с нужным tags: уезжают на ваш VPS. Shared runners можно оставить для лёгких джоб, тяжёлые — на свой тег vps-docker.
Минусы своего раннера тоже честные: вы отвечаете за диск, обновления Docker, изоляцию и мониторинг. Если пайплайн раз в неделю на 3 минуты — SaaS проще. Если CI живёт каждый день — VPS окупается быстро.
Какой VPS под runner (CPU, RAM, диск под сборки)
Runner — не веб-сервер с ровным RPS. Нагрузка пиковая: пока джоба нет, машина почти простаивает; когда пришёл npm ci && npm run build, нужны ядра, RAM и быстрый диск одновременно.
Ориентиры по размеру:
| Профиль | CPU | RAM | Диск | Что тянет |
|---|---|---|---|---|
| Лёгкий | 2 vCPU | 4 ГБ | 40–60 ГБ NVMe | lint, мелкий Node/Go |
| Средний | 4 vCPU | 8 ГБ | 80–120 ГБ | фронт + тесты, Docker build |
| Тяжёлый | 6–8+ vCPU | 16 ГБ | 160+ ГБ | monorepo, Maven, e2e в контейнерах |
CPU. Docker executor поднимает контейнер на каждую джобу. Параллельные джобы (concurrent в config.toml) умножают потребность в ядрах. Два параллельных npm run build на 2 vCPU будут драться за время — лучше 4 ядра и concurrent = 2, чем 2 ядра и очередь из пяти MR.
RAM. Node и Java любят память. Оставьте запас: система + Docker daemon + сам gitlab-runner + кэш слоёв. На 4 ГБ один тяжёлый webpack уже упрётся в OOM; 8 ГБ — комфортный минимум для нормального фронтового CI.
Дск. Здесь чаще всего «взрывается» runner, который год жил без политики очистки. Нужно место под:
- образы и слои Docker (
/var/lib/docker); - кэш runner (
/var/lib/gitlab-runner/cacheили volume); - артефакты до выгрузки в GitLab;
- временные build-директории.
NVMe-класс диска заметно ускоряет npm ci, распаковку зависимостей и Docker layer pull. Для CI важна не только ёмкость, но и IOPS: на медленном HDD пайплайн «висит» на I/O, хотя CPU свободен.
Сеть: pull образов с Docker Hub / GHCR и git clone большого репо чувствительны к каналу. На тарифах с честной полосой и шейпингом после лимита (например, 3 ТБ с последующим ограничением до 1 Мбит/с) следите за трафиком CI — зеркалирование базовых образов в свой registry сильно снижает сюрпризы в конце месяца.
География: ДЦ ближе к разработчикам и к registry ускоряет clone и push артефактов. Москва (Нагатинский) удобна для команд в РФ; для европейской аудитории — локации в DE/Франкфурте. Выдача VPS за минуты (порядка ≤120 с) позволяет поднять runner «на вчера», а не ждать тикет в поддержку.
Класс железа Ryzen + NVMe хорошо стыкуется с пиковыми сборками: много ядер на параллельные джобы и быстрый диск под node_modules и Docker layers.
Docker и установка runner (скелет команд Ubuntu)
Предполагаем Ubuntu 22.04/24.04 с уже настроенным доступом по SSH, обновлениями и минимальным firewall. Пошаговую базу — ключи, UFW, Docker как сервис — см. в гайде по настройке VPS. SSH от брута — в статье про fail2ban. Здесь — только runner и то, что ему нужно для Docker executor.
1. Docker Engine (если ещё не стоит):
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world
2. Официальный пакет GitLab Runner:
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh \
| sudo bash
sudo apt install -y gitlab-runner
sudo systemctl enable --now gitlab-runner
gitlab-runner --version
Пользователь gitlab-runner создаётся пакетом. Для Docker executor ему нужен доступ к сокету Docker — обычно через группу docker:
sudo usermod -aG docker gitlab-runner
sudo systemctl restart gitlab-runner
Не путайте это с «дать runner root на хост»: группа docker по сути эквивалентна root на машине с точки зрения компрометации. Дальше в разделе про безопасность — как сузить ущерб.
Проверка, что демон жив:
sudo gitlab-runner status
sudo journalctl -u gitlab-runner -n 50 --no-pager
Регистрация и теги
Регистрация связывает бинарник на VPS с проектом или группой в GitLab. Токен берёте в UI: Settings → CI/CD → Runners (проект) или на уровне группы. Для GitLab 16+ чаще используют authentication token нового типа runner’а, а не устаревший registration token — смотрите актуальную подсказку в интерфейсе.
Интерактивно:
sudo gitlab-runner register
Скрипт спросит URL (https://gitlab.com/ или URL вашего инстанса), токен, описание, теги и executor. Для этой статьи выбирайте docker.
Неинтерактивный скелет (удобно в cloud-init / Ansible):
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--token "GLRT_xxxxxxxx" \
--executor "docker" \
--docker-image "docker:24-cli" \
--description "vps-docker-ci" \
--tag-list "vps,docker,ci" \
--run-untagged="false" \
--locked="false"
Теги — главный рубильник. В .gitlab-ci.yml:
build:
stage: build
tags:
- vps
- docker
image: node:20-bookworm
script:
- npm ci
- npm run build
Если теги не совпали — джоба висит в pending вечно (или уедет на shared, если разрешены untagged). Имеет смысл:
- не ставить
run-untagged=trueна боевом runner’е с секретами; - разделить теги:
vps-light/vps-heavy, илиnode/java; - для protected branches — protected runners в UI GitLab, чтобы MR из fork не крутились на машине с deploy-ключами.
Конфиг лежит в /etc/gitlab-runner/config.toml. После правок:
sudo gitlab-runner verify
sudo systemctl restart gitlab-runner
Параметр concurrent на уровне всего runner’а ограничивает число одновременных джоб на этой машине — подгоните под CPU/RAM из таблицы выше.
Docker executor и безопасность (privileged, изоляция)
Docker executor для каждой джобы поднимает контейнер из image: в .gitlab-ci.yml (или дефолтный образ из config.toml). Рабочая директория монтируется внутрь, скрипты выполняются там. Это удобно: на хосте не нужно ставить Node/Java «в систему».
Privileged mode. В config.toml иногда включают:
[[runners]]
[runners.docker]
privileged = true
Это нужно для DinD (Docker-in-Docker): сборка образов через docker build внутри джобы. Цена — контейнер джобы почти как root на хосте: можно смонтировать хостовые диски, убежать из изоляции. Если runner видит несколько проектов или принимает MR извне — privileged по умолчанию выключен и так и оставьте.
Альтернативы безопаснее:
- Kaniko или Buildah для сборки образов без privileged;
- Docker socket binding (
/var/run/docker.sock) — тоже опасно (root на демоне), но иногда контролируемее DinD; не давайте его untrusted пайплайнам; - отдельный runner только для trusted/protected веток с privileged, остальное — без него.
Другие рычаги изоляции в [runners.docker]:
[runners.docker]
image = "alpine:3.20"
privileged = false
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache"]
shm_size = 0
# network_mode = "bridge"
# pull_policy = "always"
Рекомендации:
pull_policy = "always"или"if-not-present"осознанно: always снижает риск устаревшего локального тегаlatest;- не монтируйте хостовые
/,/etc, SSH-ключи в volumes «для удобства»; - секреты — через CI/CD Variables (masked/protected), не через файлы на диске runner’а;
- отдельный VPS под CI лучше, чем runner на том же хосте, где крутится прод-БД.
Компрометация пайплайна (supply-chain в зависимостях, вредоносный MR) при доступе к Docker socket = компрометация хоста. Держите runner на выделенной машине, обновляйте Docker и gitlab-runner, ограничивайте, кто может запускать пайплайны на этом теге.
Кэш и артефакты (место на диске!)
Два разных механизма, оба любят диск.
Cache в GitLab CI — ускорение повторных джоб (node_modules, .m2, Gradle cache). Для Docker executor кэш обычно лежит на хосте или в named volume и не считается надёжным хранилищем: его можно вытеснить, ключ может не совпасть. Это ок для скорости, плохо как единственная копия артефакта.
Artifacts — файлы, которые GitLab забирает после джобы (билд, отчёты, coverage) и хранит у себя по expire_in. Пока джоба бежит и пока runner готовит upload, они занимают место на VPS.
Типичная картина через месяц без политики:
/var/lib/dockerраздут старыми образами и build cache;- volume кэша runner’а на десятки гигабайт;
- забытые
docker system dfпоказывают reclaimable 40+ ГБ.
Профилактика:
# Что занимает Docker
sudo docker system df -v
# Осторожная регулярная чистка (cron раз в неделю)
sudo docker system prune -af --filter "until=168h"
В config.toml можно ограничить кэш и задать volumes явно. В .gitlab-ci.yml — короткие expire_in для артефактов и осмысленные cache:key, чтобы не плодить уникальные кэши на каждый commit SHA без нужды.
Когда диск всё же упёрся в 100% — сначала диагностика df/du, journal и Docker prune; разбор по полочкам в статье «Диск VPS 100%». Runner с полным диском начинает падать странными ошибками no space left посреди npm ci — лучше алерт на 85%, чем ночной разбор.
Отдельный диск или хотя бы запас 2× от «рабочего» размера кэша экономит нервы. Для среднего профиля 80–120 ГБ NVMe — не роскошь, а запас под слои и пару параллельных сборок.
Мониторинг очереди и занятости
Self-hosted CI «молчит», пока вы не смотрите. Полезные сигналы:
- Pending джобы в GitLab — UI пайплайна или API. Если pending растёт, а runner online — мало
concurrentили джоба ждёт несуществующий тег. - Статус runner’а в CI/CD → Runners: online/offline, last contact.
- Нагрузка VPS: CPU steal/iowait, RAM, диск.
# Локально на VPS
sudo gitlab-runner list
sudo gitlab-runner status
htop
df -h / /var/lib/docker
Имеет смысл держать простой мониторинг uptime + диск + CPU: когда runner «offline» после ребута или диск 95%, вы узнаете до того, как разработчики напишут в чат. Базовый набор идей — в материале про мониторинг VPS.
Метрики самого runner’а: GitLab Runner умеет отдавать Prometheus metrics (listener в config.toml). Даже без полного стека можно скрейпить ci_runner_jobs и алертить на долгий busy. Минимум без Prometheus — cron, который чекает gitlab-runner status и свободное место, шлёт в Telegram/email.
Очередь vs параллелизм: если MR копятся, либо поднимите concurrent (и железо), либо заведите второй runner с тем же тегом — GitLab сам разложит джобы. Два мелких VPS иногда гибче одного жирного: можно обновлять по очереди без полного простоя CI.
Смета: свой VPS vs минуты GitLab.com
Считайте не «цену гигабайта», а минуты CI в месяц × стоимость минуты на вашем тарифе GitLab против фиксированной платы за VPS.
Грубая модель:
- команда жжёт ~5 000 минут shared runners в месяц;
- на платном тарифе минуты сверх пакета стоят ощутимо (смотрите актуальный прайс GitLab — цифры меняются);
- VPS 4 vCPU / 8 ГБ NVMe под runner работает 24/7 за предсказуемые деньги (линейка VPS от 360₽, выдача порядка пары минут).
Даже если «сырые» минуты на бумаге дешевле, на своём runner’е вы:
- не упираетесь в monthly cap посреди спринта;
- кэшируете зависимости локально (меньше повторных download-минут);
- параллелите столько, сколько тянет железо, без конкуренции с чужими shared-воркерами в пике.
Когда SaaS всё ещё выгоднее: редкие пайплайны, нет желания админить Docker, нужен нулевой ops. Когда свой VPS выигрывает: ежедневный CI, тяжёлые сборки, несколько проектов на одном runner’е с тегами, требования к изоляции данных.
Скрытые расходы self-hosted: время на обновления, риск забить диск, необходимость бэкапа config.toml и учёта токенов. Заложите в смету час-два админки в месяц — всё равно обычно дешевле постоянной докупки минут.
Чеклист перед запуском и VPS под CI
Перед тем как отдать тег vps в прод-пайплайны:
- Ubuntu обновлена, SSH по ключам, UFW только нужные порты — базовая настройка.
- Fail2ban на SSH — краткий гайд.
- Docker Engine установлен,
hello-worldпроходит. gitlab-runnerустановлен, в группеdocker, сервис enabled.- Runner зарегистрирован с явными тегами,
run-untagged=false. privileged=false, пока нет осознанной нужды; для образов — Kaniko/Buildah.concurrentсогласован с CPU/RAM.- Политика очистки Docker + запас места; алерт по диску.
- Тестовый пайплайн с
tags:прошёл green; shared runners не подхватывают секретные джобы. - Мониторинг online-статуса runner’а и свободного места.
Для сборок удобен VPS с Ryzen-классом CPU и NVMe: пиковые компиляции и параллельные джобы любят ядра и IOPS. Площадки AZERTA: выдача обычно до 120 секунд, тарифы от 360₽, поддержка 24/7, ДЦ в Москве (Нагатинский) и локации в DE/Франкфурте; на трафике ориентир 3 ТБ с шейпингом до 1 Мбит/с после лимита — заложите зеркала образов, если CI качает много слоёв.
Если нужен сервер сразу под Docker и runner без долгой выдачи — посмотрите VPS для разработки: поднять машину, поставить executor по чеклисту выше и увести тяжёлые пайплайны с shared minutes на свой тег.
FAQ
Нужен ли отдельный VPS только под GitLab Runner?
Желательно. Runner с Docker-сокетом — высокая ценность для атаки. На той же машине, что прод, ошибка в CI или зависимостях бьёт по боевым данным. Отдельный небольшой VPS под CI проще обновлять и ограничивать по сети.
Можно ли один runner на несколько проектов?
Да: регистрируйте на уровне группы или добавляйте runner в несколько проектов в UI. Разведите теги и protected runners, чтобы чужой pet-проект не крутил джобы с prod-секретами.
Docker-in-Docker обязателен для сборки образов?
Нет. Privileged DinD — привычный, но рискованный путь. Kaniko, Buildah или отдельный build-агент закрывают большинство задач без privileged=true.
Почему джоба висит в pending?
Чаще всего не совпали tags, runner offline, или concurrent занят другими джобами. Проверьте UI Runners, gitlab-runner status и теги в .gitlab-ci.yml.
Как не забить диск за месяц?
Лимиты на expire_in артефактов, осмысленные cache key, cron с docker system prune, мониторинг df на /var/lib/docker. Подробный разбор заполнения диска — в отдельной статье.
Shared runners и свой runner могут жить вместе?
Да. Лёгкие джобы без тегов — на shared; тяжёлые и секретные — с tags: [vps]. Так вы экономите минуты SaaS и не тащите весь CI на одну машину сразу.