· 15 мин чтения

Self-hosted runners для GitHub Actions: CI/CD на собственном железе без лимитов

Self-hosted runners для GitHub Actions: CI/CD на собственном железе без лимитов

Привет! На связи Семёнов Евгений Сергеевич, директор ITFresh. Только представьте: в прошлом месяце наша команда провернула ещё один крутой проект по миграции CI/CD. Мы взяли команду из десяти разработчиков e-commerce платформы и перевели их с платных GitHub Actions runners на наши собственные, размещённые прямо в дата-центре. Результат? Не просто экономия в 34 000 рублей в месяц, но и сборка образов стала летать в 2,7 раза быстрее! Хотите узнать, как мы это сделали? Тогда я с удовольствием поделюсь нашим опытом.

Когда собственные раннеры реально нужны

Казалось бы, 2000 бесплатных минут в месяц на публичных репозиториях и 3000 на Pro-планах от GitHub — это щедро. Ну куда их девать? Но подождите, пока ваш монорепо не разрастётся до таких размеров, что каждый PR будет собираться по 20 минут! Вот тут-то и начинается настоящее веселье: лимит сгорает за неделю, а мы вынуждены лезть в карман за платными слотами. Каждая минута стоит $0,008. А если вам нужен Windows, умножайте на два. macOS? Ого, там все десять!

Как-то раз у нас был клиент — отличный финтех-стартап из Москвы, в команде 14 разработчиков. И что вы думаете? Их ежемесячный счёт за минуты GitHub просто зашкаливал, достигая $470! Мы не стали тянуть: взяли старый добрый Dell R640 из их серверной, накатили на него три self-hosted раннера, и вуаля! Уже через месяц оплата за лицензию превратилась в ноль. Просто и эффективно.

Кроме денег есть ещё причины:

Установка агента на Linux

Сам процесс, на самом деле, не сложный. Заходите в настройки репозитория или организации, ищете «New self-hosted runner». Получаете токен, команды — и вот она, заветная ссылка на tarball. Для тех, кто на Ubuntu 22.04 LTS и пользуется systemd, всё выглядит именно так.

sudo useradd -m -s /bin/bash github-runner
sudo -iu github-runner

mkdir actions-runner && cd actions-runner
curl -o actions-runner.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-x64-2.321.0.tar.gz
tar xzf actions-runner.tar.gz

./config.sh --url https://github.com/yourorg/yourrepo \
  --token AAAA... \
  --name runner-dell-r640-01 \
  --labels self-hosted,linux,x64,docker \
  --work _work \
  --unattended

exit  # выходим из под github-runner
sudo /home/github-runner/actions-runner/svc.sh install github-runner
sudo /home/github-runner/actions-runner/svc.sh start

Всё. Через 10 секунд агент светится зелёным в UI. Systemd-юнит переживает ребут, логи пишутся в _diag/.

Запуск в Docker-контейнере

Я предпочитаю контейнерный подход — проще поднимать копии, проще изолировать. Использую готовый образ myoung34/github-runner с автоматической регистрацией:

docker run -d --restart=always \
  --name gha-runner-01 \
  -e REPO_URL="https://github.com/yourorg/yourrepo" \
  -e RUNNER_NAME="runner-docker-01" \
  -e RUNNER_TOKEN="AAAA..." \
  -e RUNNER_WORKDIR="/tmp/runner/work" \
  -e LABELS="self-hosted,linux,docker" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /tmp/runner/work:/tmp/runner/work \
  myoung34/github-runner:latest

Флаг -v /var/run/docker.sock:... разрешает сборку образов изнутри job, но по сути даёт root на хост. Для доверенных команд это нормально. Для open-source — лучше DinD или Podman rootless.

Windows-агент и MSI-пакеты

Для .NET-проектов, конечно, без Windows-раннера никуда. Принцип установки тот же, только вместо Bash у нас PowerShell. Лично я предпочитаю ставить их на Server 2022 Core — меньше лишнего, а значит, и поверхность атаки сокращается.

# PowerShell от имени администратора
New-Item -Path C:\actions-runner -ItemType Directory
Set-Location C:\actions-runner

Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-win-x64-2.321.0.zip `
  -OutFile actions-runner.zip
Expand-Archive -Path actions-runner.zip -DestinationPath .

.\config.cmd --url https://github.com/yourorg/yourrepo `
  --token AAAA... `
  --name win-runner-01 `
  --labels self-hosted,windows,x64,dotnet `
  --runasservice

# Сервис называется actions.runner.*, стартует автоматически

Метки, Runner Groups и маршрутизация

Всё самое интересное начинается, когда раннеров становится больше трёх. Как быть, если нужно направить тяжёлые Docker-сборки на мощный хост, а простые lint-проверки — на слабенький? Вот тут-то на помощь и приходят метки! Умное решение, не правда ли?

В workflow пишем:

jobs:
  build-image:
    runs-on: [self-hosted, linux, docker, high-cpu]
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t app:${{ github.sha }} .

  lint:
    runs-on: [self-hosted, linux, lightweight]
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

Регистрируете раннер с нужным набором --labels — GitHub сам выберет подходящий. Runner Groups (в Enterprise/Team-планах) добавляют слой: можно запретить репозиторию использовать определённые группы раннеров.

СценарийМеткиЖелезо
Docker build, тесты integrationself-hosted, linux, docker, high-cpu16 vCPU, 32 ГБ RAM, SSD
Lint, typecheckself-hosted, linux, lightweight2 vCPU, 4 ГБ RAM
Сборка .NETself-hosted, windows, dotnet8 vCPU, 16 ГБ RAM
ARM-кросскомпиляцияself-hosted, linux, arm64Raspberry Pi 5 / Ampere Altra
GPU-инференсself-hosted, linux, gpu, cuda12RTX 4090 или A100

Автоскейлинг через ARC в Kubernetes

А что, если команда разрослась до двадцати человек и больше, а параллельных PR стало пятнадцать плюс? Обычные статичные раннеры тут просто выдыхаются. Вот тут и приходит на выручку наш спаситель — Actions Runner Controller, или ARC! Он умеет по требованию поднимать поды-раннеры, когда они действительно нужны, и аккуратно гасить их после простоя. Просто фантастика!

helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
helm install arc actions-runner-controller/gha-runner-scale-set \
  --namespace arc-runners --create-namespace \
  --set githubConfigUrl="https://github.com/yourorg" \
  --set githubConfigSecret.github_token="ghp_XXXX" \
  --set minRunners=2 --set maxRunners=20

Каждый job получает свой под из шаблона, а после завершения работы под удаляется. Чтобы первый PR дня не ждал холодного старта, мы держим минимум два раннера постоянно в работе. Наш клиент с таким пайплайном теперь видит, как пиковые 15 параллельных сборок пролетают за 7 минут вместо прежних 28, когда у них было всего три статичных агента.

Безопасность — что обязательно проверить

Self-hosted runner — это по сути машина, которая выполняет код прямо из вашего репозитория. Помните: любой, у кого есть write-права, может запросто запушить workflow, скачивающий бэкдор. Поэтому будьте крайне осторожны.

Кейс: стриминговый сервис, 40+ сборок в день

Однажды, осенью 2025 года, к нам обратился клиент. Ребята занимались стримингом видео, команда из 11 разработчиков, и работали они с монорепо на Go и React. Их CI крутился на shared runners, и, честно говоря, ситуация была неважная: по 8 долларов за рабочий день улетали в трубу, а эти 20-минутные очереди в рабочее время? Ребята просто забыли, что такое быстрая разработка.

Что мы сделали? Развернули два мощнейших физических раннера на серверах Dell Xeon Platinum 8280, прямо в нашей стойке МТС — каждый с 48 vCPU и 128 ГБ RAM. Сверху, в отдельном Kubernetes-кластере, накатили ARC. Чтобы уж точно никаких узких мест, специально подвезли 40G Mellanox аплинк: теперь docker pull из внутреннего Harbor-регистра гарантированно не будет упираться в потолок скорости. И, конечно, не забыли про один Windows-раннер для сборки их Electron-клиента. Всё по уму!

Результаты? Они просто поразили! Среднее время PR сократилось с 18 до невероятных 5 минут. А очереди, от которых они так страдали, исчезли совсем. GitHub-счёт, который раньше был $480, теперь упал до смешных $12 — остались только расходы на secrets scanning и минуты на публичные репо. Вся эта работа заняла всего 4 дня, а ценник проекта составил 95 000 рублей. По-моему, отличная сделка!

Настроим self-hosted runners и разгрузим бюджет

Спроектирую и подниму парк собственных раннеров GitHub Actions: физика или Kubernetes, Docker или VM-based, с автоскейлингом, метками и безопасной конфигурацией. Интеграция с внутренним Harbor/GitLab Registry, ARC в любом кластере. Обычно 2–5 дней работ.

Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш

FAQ — частые вопросы по раннерам

Можно ли один раннер подключать к нескольким репозиториям?
Да. Регистрируйте раннер на уровне организации в Settings → Actions → Runners. Дальше — метки и Runner Groups для фильтрации репозиториев. Такой подход удобнее, чем плодить по отдельному раннеру для каждого проекта.
Нужен ли белый IP-адрес для self-hosted runner?
Не нужен. Раннер держит исходящее long-polling соединение к github.com через 443 порт. Входящих подключений нет, NAT и файрволл не мешают. Разрешите исходящий HTTPS на github.com и *.actions.githubusercontent.com — этого достаточно.
Как обновляется раннер?
GitHub сам обновляет агент при старте нового job, если доступна свежая версия. За прокси или с отрезанным интернетом скачивайте tarball руками и запускайте ./config.sh --replace. Если раннер под systemd — обновление применится после перезапуска сервиса.
Не опасно ли монтировать docker.sock в раннер?
Монтирование /var/run/docker.sock равносильно root-правам на хосте. Для приватных репозиториев с доверенными разработчиками — допустимо. Если есть сомнения: rootless Docker, Podman или DinD с отдельным daemon внутри контейнера раннера.
Сколько параллельных job тянет один раннер?
Один экземпляр агента обслуживает ровно одну job одновременно. Хотите параллелизм — запускайте N экземпляров в отдельных директориях с уникальными именами. Бюджет памяти — 250–500 МБ на агент плюс потребление самой сборки.

Подпишитесь на рассылку ITfresh

Ищете практические гайды? Раз в неделю мы делимся ими для руководителей IT и сисадминов: безопасность, 1С, миграции, резервные копии и, конечно, лайфхаки из наших реальных проектов. Подписывайтесь!

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.