CI без GitHub Actions: свои раннеры на собственном сервере
Когда облачные раннеры GitHub Actions начинают вести себя непредсказуемо — очередь растягивается на часы, платёж не проходит, а IP-адреса раннеров периодически недоступны из-за сетевых ограничений — команда упирается в вопрос: чем это заменить и сколько своих ресурсов на это уйдёт. Собственные раннеры на арендованном сервере снимают зависимость от чужой очереди и биллинга, но добавляют работу по администрированию. Ниже — практический разбор: как выбрать платформу, посчитать сервер под сборки, перенести пайплайны и не подорваться на граблях с секретами и кэшем.
Содержание
Какую платформу выбрать
Есть два разных сценария, и их не стоит путать.
Сценарий 1: репозитории остаются на GitHub, меняются только раннеры. GitHub поддерживает self-hosted runners "из коробки" — это обычный агент (actions-runner), который вы регистрируете в репозитории или организации и запускаете на своём сервере. Пайплайны (.github/workflows/*.yml) переписывать не нужно — меняется только runs-on с ubuntu-latest на имя раннера или лейбл (self-hosted, linux, x64). Это самый низкий порог входа: если единственная проблема — недоступность или нестабильность облачных раннеров GitHub, а сам GitHub как хостинг репозитория устраивает, этого достаточно.
Сценарий 2: уходите с GitHub целиком — из-за платежей, ограничений доступа к самому сервису или желания держать код и историю сборок на своей инфраструктуре. Тогда нужна связка "git-хостинг + CI" на своём сервере:
| Платформа | Git-хостинг | CI встроен | Синтаксис пайплайна | Ресурсоёмкость |
|---|---|---|---|---|
| GitLab CE | да | да (GitLab CI) | .gitlab-ci.yml, близко к GitHub Actions по логике | высокая (от 4 ГБ RAM только под сам GitLab) |
| Forgejo / Gitea + Actions | да | да (act_runner, почти 1-в-1 синтаксис GitHub Actions) | .gitea/workflows/*.yml, YAML почти идентичен | низкая (1-2 ГБ RAM хватает под сам сервис) |
| Woodpecker CI | нет, подключается к внешнему git | да | .woodpecker.yml, свой формат | низкая |
| Drone CI | нет, подключается к внешнему git | да | .drone.yml, свой формат | низкая, но проект развивается медленнее |
| Jenkins | нет | да | Groovy Jenkinsfile или UI-джобы | средняя-высокая, гибкость ценой сложности |
Если у вас привычка к синтаксису GitHub Actions и не хочется переписывать половину workflow-файлов — присмотритесь к Forgejo Actions (форк Gitea с более активным независимым развитием): его act_runner понимает большинство uses:-шагов из GitHub Actions, включая часть action'ов из marketplace, через runner-образ на базе act. Совместимость не стопроцентная — сложные composite actions и часть JavaScript-actions могут не завестись без правок, — но для пайплайна "тесты → сборка образа → деплой" переезд обычно занимает часы, а не дни.
Если пайплайны у вас изначально писались под Jenkins или в компании уже есть GitLab-компетенция — логичнее остаться в этой экосистеме и просто перенести раннеры/executors на свой сервер, не меняя платформу целиком.
Сервер под сборки: сколько ресурсов закладывать
Раннер — это не веб-сервис с ровной нагрузкой, а машина, которая либо простаивает, либо жрёт CPU и диск на полную во время сборки. Считать нужно от пиковой параллельности, а не от среднего.
CPU. Типовая джоба (тесты + сборка Docker-образа для среднего backend-сервиса) на сервере с честными выделенными ядрами (не overselling-vCPU) укладывается в 2-4 ядра. Если параллельно должно идти 3 джобы (feature-ветка, master, nightly-сборка) — закладывайте 8-12 ядер, чтобы сборки не выстраивались в очередь и не конкурировали за CPU и I/O. Компиляция C/C++/Rust съедает заметно больше, чем сборка Node.js/Python/PHP-проекта — если такие джобы есть в стеке, проверьте реальное время сборки на кандидате сервера до переезда, а не полагайтесь на паспортные характеристики CPU.
Диск. Это тот ресурс, о который обычно и разбиваются self-hosted раннеры. Считайте отдельно:
- слои Docker-образов, которые накапливаются при каждой сборке (без регулярной чистки
docker system pruneдиск забивается за недели, а не месяцы); - кэш зависимостей (
node_modules,.m2,~/.cache/pip,vendor/для PHP) — при параллельных джобах на один и тот же кэш может претендовать несколько процессов; - рабочие копии репозиториев (checkout) — при большом монорепо и параллельных джобах это может быть несколько гигабайт на джобу;
- логи сборок, если раннер их не выгружает во внешнее хранилище.
Для команды из 5-10 разработчиков с несколькими сервисами закладывайте от 100 ГБ NVMe только под кэш и слои образов, не считая ОС и самого CI-сервиса. NVMe здесь не роскошь: сборка с кэшем на HDD или сетевом диске с высокой задержкой ощутимо медленнее — кэш зависимостей и слои Docker дают много случайного чтения/записи.
Параллельность джобов. Настраивается через concurrent (GitLab Runner), число раннеров/воркеров (Woodpecker, Drone) или лимит job у act_runner. Правило, которое стоит проверить на своём железе, а не взять как готовую цифру: параллельные джобы × ресурсы одной джобы не должны превышать ядра и RAM сервера с запасом 20-30% на сам CI-сервис и Docker daemon. Превысите — джобы не упадут с ошибкой, а начнут выполняться в разы дольше из-за конкуренции за CPU и своп.
Если сомневаетесь, с чего начинать расчёт конфигурации под разработку и CI/CD в целом — сначала прикиньте пиковую параллельность по факту (сколько джоб реально идёт одновременно в час пик), а уже потом переводите её в ядра и RAM с запасом.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПеренос существующих пайплайнов
Начинать стоит не с переписывания YAML, а с инвентаризации того, что вообще делают текущие workflow — потому что часть логики обычно спрятана в сторонних action'ах, которых на self-hosted платформе может не быть.
Порядок, который снижает риск сюрпризов в проде:
- Выпишите все
uses:из workflow-файлов — экшены для checkout, кэширования, публикации артефактов, деплоя. Для каждого проверьте, есть ли эквивалент на целевой платформе: у GitLab CI это встроенные ключевые слова (cache:,artifacts:) вместо экшенов; у Forgejo Actions — часто тот же самый action работает как есть, если он не завязан на GitHub-специфичное API. - Разделите джобы на "чистая логика" и "интеграция с платформой". Шаги сборки, тестов и линтинга обычно переносятся почти дословно — это просто команды в shell. Шаги вроде "создать GitHub Release" или "закомментировать PR" — платформо-специфичны и требуют замены на нативный аналог или webhook.
- Мигрируйте один пайплайн целиком, а не все сразу. Возьмите наименее критичный сервис, доведите его CI на новой платформе до зелёного статуса на реальных коммитах в течение недели, и только потом переносите остальные.
- Держите старый CI живым, пока новый не отработал минимум пару полных релизных циклов. Параллельный запуск обеих систем несколько недель — не избыточная осторожность, а способ поймать различия в поведении (другая версия Docker, другой набор переменных окружения) до того, как они сорвут релиз.
Для GitLab CI отдельно есть пошаговый разбор установки раннера: GitLab CI runner на Ubuntu 24.04 — пошаговая установка — пригодится, если решите остаться в экосистеме GitLab, но раннеры вынести на свой сервер.
Отдельно проговорите с командой расхождения в переменных окружения: GITHUB_SHA/GITHUB_REF у GitHub Actions не совпадают с CI_COMMIT_SHA/CI_COMMIT_REF_NAME у GitLab CI или Woodpecker. Скрипты, которые парсят эти переменные напрямую, придётся поправить руками — это не автоматизируется надёжно.
Секреты: где хранить и как не хранить
Секреты — самое частое место, где self-hosted CI подводит команду, потому что "секрет в переменной окружения раннера" кажется рабочим решением ровно до первого инцидента.
Базовый уровень — встроенное хранилище платформы. У GitLab CI это защищённые CI/CD-переменные (Settings → CI/CD → Variables) с флагами "Protected" (доступны только защищённым веткам/тегам) и "Masked" (значение маскируется в логах, если совпадает по формату). У Forgejo/Gitea Actions — секреты репозитория или организации, доступные в workflow через ${{ secrets.NAME }}. У Woodpecker — секреты через woodpecker-cli secret add с привязкой к репозиторию и, при необходимости, к конкретному event (push, tag, pull_request).
Маскирование в логах — не панацея: оно работает по точному совпадению строки, и если секрет попадёт в лог в изменённом виде (base64, с добавленным префиксом, разбитый переносом строки) — маска его не поймает. Разбор одного такого случая на практике: секрет утёк в лог сборки — шесть часов ротации ключей — стоит прочитать вместе с настройкой CI, а не после первого инцидента.
Что стоит сделать поверх встроенного хранилища:
- не логировать секреты через
set -xв shell-скриптах джобы — самая частая причина утечки, не сама платформа; - ограничивать доступность секретов веткой/окружением (production-секреты не должны быть видны в pull-request джобах от форков — классическая дыра, через которую воруют токены);
- заводить отдельные узкие по правам токены под CI (deploy-токен вместо личного токена разработчика с полным доступом) и ротировать их по расписанию, а не "когда вспомнили";
- для крупной инфраструктуры — вынести секреты во внешнее хранилище (Vault, Infisical) и подключать их к раннеру через плагин или CLI на этапе job, а не хранить копии везде сразу.
Общая практика ротации токенов и ключей, применимая не только к CI, — плановая замена по календарю, а не только по факту утечки, и обязательный отзыв старого значения сразу после выпуска нового, а не «когда-нибудь потом».
Кэширование зависимостей
Без кэша self-hosted CI проигрывает облачному по одному конкретному параметру — времени сборки, и разница ощущается уже на второй-третьей неделе, когда разработчики начинают ждать пайплайн по 10-15 минут вместо привычных двух-трёх.
Основные слои кэша, которые стоит настроить сразу:
- Кэш зависимостей пакетного менеджера —
node_modules/кэш npm,.m2для Maven,~/.cache/pipилиvenv,vendor/bundleдля Ruby. В GitLab CI это ключcache:сkeyна основе хэша lock-файла (package-lock.json,poetry.lock) — при неизменном lock-файле кэш переиспользуется, при изменении пересобирается заново. - Кэш слоёв Docker-образа — через BuildKit (
--cache-from/--cache-toс типомlocalилиregistry) или через встроенный registry-кэш, если у вас поднят собственный приватный Docker registry. Без этого каждая сборка образа перекачивает и переустанавливает системные пакеты заново, даже если Dockerfile не менялся. - Кэш между джобами одного пайплайна — если тесты и сборка идут отдельными джобами, но используют одни и те же зависимости, передавайте их через артефакты джобы, а не пересобирайте дважды.
Важный нюанс, который на self-hosted платформе легко упустить: общий кэш на одном сервере между параллельными джобами — источник трудноуловимых багов, если кэш не изолирован по ветке или пайплайну. Джоба из feature-ветки может подложить в общий кэш зависимость нестабильной версии, и следующая сборка master получит её же — без явной ошибки, просто с другим поведением. Один такой случай на практике: кэш зависимостей был отравлен — три недели никто не видел. Разделяйте ключи кэша по ветке или хотя бы по хэшу lock-файла, а не используйте один общий ключ "на всех".
Отдельно закладывайте место под сам кэш и его ротацию — бесконтрольно растущий кэш зависимостей на self-hosted раннере со временем занимает больше диска, чем сами репозитории. Настройте cache: max-size (или аналог у вашей платформы) и периодическую очистку неиспользуемых записей — вручную через cron или встроенным механизмом платформы, если он есть.
Масштабирование под пиковую нагрузку
Облачный CI прятал от вас один вопрос: что происходит, когда десять разработчиков пушат одновременно перед дедлайном и очередь джоб внезапно утраивается. На своём сервере этот вопрос встаёт в полный рост.
Практические варианты, от простого к сложному:
- Несколько раннеров на одном сервере с тегами. Простой шаг — завести 2-3 экземпляра раннера с разными тегами (
fast,heavy,deploy) и распределять джобы через конфиг пайплайна. Не решает проблему пиковой нагрузки саму по себе, но даёт контроль над тем, какие джобы конкурируют за ресурсы. - Второй сервер под раннеры, подключённый к тому же CI. Большинство платформ (GitLab CI, Forgejo Actions, Woodpecker) поддерживают дополнительный раннер без изменений в самой платформе — просто ещё один агент. Предсказуемое горизонтальное масштабирование "руками" — разумный шаг, если один сервер уже упирается в лимит CPU в часы пик.
- Автомасштабирование через executor с динамическими воркерами. GitLab Runner поддерживает Docker Machine executor (устаревающий, но пока рабочий) или Kubernetes executor поверх своего кластера — раннер сам поднимает и гасит воркеры под нагрузку. Для команды до 15-20 разработчиков это обычно лишняя сложность, которая не окупается: разумнее остаться на варианте 2 дольше, чем кажется правильным.
Ориентир, а не точная цифра, которую стоит перепроверить на своей нагрузке: команда из 8-10 активных разработчиков с несколькими сервисами обычно укладывается в сервер с 12-16 ядрами и параллельностью 4-6 джоб, если кэш зависимостей и слоёв настроен. Момент, когда экономика "свой CI" перестаёт быть очевидной, разобран в статье свой GitLab против платных тарифов: точка перелома — она про GitLab, но логика расчёта применима к любой self-hosted CI-платформе.
Для предсказуемых пиков (релизная неделя, конец квартала) проще временно поднять второй сервер под раннеры на несколько дней и снести его после, чем держать избыточные мощности круглый год.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли оставить репозитории на GitHub, но полностью уйти от облачных раннеров GitHub Actions?
Да, это самый частый и наименее трудозатратный сценарий — self-hosted runner GitHub Actions регистрируется в настройках репозитория или организации, а workflow-файлы почти не меняются: достаточно поправить runs-on на лейбл вашего раннера.
Нужно ли переписывать весь пайплайн при переходе на Forgejo Actions?
Обычно нет — синтаксис workflow максимально близок к GitHub Actions. Но проверьте каждый используемый uses:-экшен на совместимость: сложные composite actions и часть JS-based экшенов из marketplace могут работать не полностью, особенно если завязаны на API самого GitHub.
Сколько по времени занимает перенос пайплайна на self-hosted CI?
Для простого сервиса (тесты + сборка образа + деплой по SSH) — от нескольких часов до одного рабочего дня, включая проверку на реальных коммитах. Для монорепо со сложной матрицей джоб и множеством сторонних экшенов — рассчитывайте на несколько дней с параллельным прогоном старого и нового CI.
Что делать, если один сервер под раннеры уже не справляется с пиковой нагрузкой?
Сначала проверьте, настроен ли кэш зависимостей и слоёв Docker — в большинстве случаев именно его отсутствие, а не нехватка CPU, оказывается причиной медленных сборок. Если кэш настроен и упор реально в ресурсы — добавляйте второй сервер с ещё одним раннером, а не сразу переходите к автомасштабированию.
Безопасно ли хранить секреты деплоя прямо в переменных CI на своём сервере?
Безопаснее, чем в облаке в смысле физического контроля, но требует тех же дисциплин: маскирование в логах, ограничение по защищённым веткам, отдельные узкие по правам токены под CI и регулярная ротация. Сам факт self-hosted не заменяет эти практики.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →