· 15 мин чтения

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

Семёнов Евгений Сергеевич, директор АйТи Фреш. Last month мы с командой добили очередной переезд CI/CD — команда разработки e-commerce платформы из десяти человек улетала с платных GitHub Actions runners на собственные раннеры в нашем дата-центре. В результате экономия 34 000 руб. в месяц и скорость сборки образов выросла в 2,7 раза. В этой статье рассказываю, как это делается на практике.

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

GitHub даёт 2000 бесплатных минут в месяц на публичных репозиториях и 3000 на Pro-планах. Звучит много, пока ваш монорепо не начинает собираться по 20 минут в PR. Дальше счётчик сгорает за неделю, и команда переходит на платные слоты по $0,008 в минуту (плюс множитель 2x для Windows и 10x для macOS).

У меня в практике был клиент — финтех-стартап в Москве, 14 разработчиков. Ежемесячный счёт на GitHub за минуты вырос до $470. Мы поставили три self-hosted раннера на старом Dell R640 с их серверной, и через месяц лицензия обнулилась.

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

Установка агента на 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-раннер. Принцип тот же, только 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

Когда команда переваливает за 20 человек и параллельных PR становится 15+, статичные раннеры перестают справляться. Тут подключается 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 поднимается pod из шаблона, после завершения — удаляется. Минимум два раннера крутятся постоянно, чтобы первый 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 каждый) и развернули поверх них ARC в отдельном Kubernetes-кластере. Подвезли 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 подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.