Назад в блог/GitLab Runner и Docker на VPS: self-hosted CI без переплаты за минуты

GitLab Runner и Docker на VPS: self-hosted CI без переплаты за минуты

Self-hosted GitLab Runner на VPS с Docker: как уйти от поминутной оплаты CI, выбрать тариф, настроить executor, кэш и безопасность.

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 здесь не дублируем — на них есть отдельные гайды; ссылки в конце соответствующих разделов.

Схема: GitLab.com → self-hosted Runner на VPS → Docker executor → кэш и артефакты
GitLab Runner и Docker на VPS: схема self-hosted CI

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 (если ещё не стоит):

BASH
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:

BASH
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:

BASH
sudo usermod -aG docker gitlab-runner
sudo systemctl restart gitlab-runner

Не путайте это с «дать runner root на хост»: группа docker по сути эквивалентна root на машине с точки зрения компрометации. Дальше в разделе про безопасность — как сузить ущерб.

Проверка, что демон жив:

BASH
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 — смотрите актуальную подсказку в интерфейсе.

Интерактивно:

BASH
sudo gitlab-runner register

Скрипт спросит URL (https://gitlab.com/ или URL вашего инстанса), токен, описание, теги и executor. Для этой статьи выбирайте docker.

Неинтерактивный скелет (удобно в cloud-init / Ansible):

BASH
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:

YAML
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. После правок:

BASH
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 иногда включают:

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]:

TOML
[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+ ГБ.

Профилактика:

BASH
# Что занимает 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 «молчит», пока вы не смотрите. Полезные сигналы:

  1. Pending джобы в GitLab — UI пайплайна или API. Если pending растёт, а runner online — мало concurrent или джоба ждёт несуществующий тег.
  2. Статус runner’а в CI/CD → Runners: online/offline, last contact.
  3. Нагрузка VPS: CPU steal/iowait, RAM, диск.
BASH
# Локально на 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 в прод-пайплайны:

  1. Ubuntu обновлена, SSH по ключам, UFW только нужные порты — базовая настройка.
  2. Fail2ban на SSH — краткий гайд.
  3. Docker Engine установлен, hello-world проходит.
  4. gitlab-runner установлен, в группе docker, сервис enabled.
  5. Runner зарегистрирован с явными тегами, run-untagged=false.
  6. privileged=false, пока нет осознанной нужды; для образов — Kaniko/Buildah.
  7. concurrent согласован с CPU/RAM.
  8. Политика очистки Docker + запас места; алерт по диску.
  9. Тестовый пайплайн с tags: прошёл green; shared runners не подхватывают секретные джобы.
  10. Мониторинг 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 на одну машину сразу.

Нужен быстрый сервер под ваш проект?

Арендуйте производительный KVM VPS в Москве или Германии на скоростных NVMe дисках от 405 ₽/мес. Автовыдача за 60 секунд, выделенный IPv4 и защита от DDoS.

Выбрать тариф