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 раннера, и вуаля! Уже через месяц оплата за лицензию превратилась в ноль. Просто и эффективно.
Кроме денег есть ещё причины:
- Приватная сеть. Если CI должен ходить в закрытый VPN, корпоративный NuGet или внутренний Docker Registry — shared runner туда не попадёт.
- Специфичное железо. GPU-сборки, ARM-кросс-компиляция, проверка на реальных ARMv7 — GitHub такое не предоставляет.
- Требования безопасности. ГОСТ-шифрование, ФСТЭК-контур, закрытые репо с ДСП-кодом — только своё железо.
- Длинные сборки. Публичные раннеры убивают job по лимиту 6 часов, 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, тесты integration | self-hosted, linux, docker, high-cpu | 16 vCPU, 32 ГБ RAM, SSD |
| Lint, typecheck | self-hosted, linux, lightweight | 2 vCPU, 4 ГБ RAM |
| Сборка .NET | self-hosted, windows, dotnet | 8 vCPU, 16 ГБ RAM |
| ARM-кросскомпиляция | self-hosted, linux, arm64 | Raspberry Pi 5 / Ampere Altra |
| GPU-инференс | self-hosted, linux, gpu, cuda12 | RTX 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, скачивающий бэкдор. Поэтому будьте крайне осторожны.
- Никогда не вешайте self-hosted runners на публичные репозитории. Pull request от форка выполнит что угодно — и вы разрешите это.
- А для приватных репозиториев есть важный лайфхак: включите «Require approval for first-time contributors» в настройках Actions. Это ваша страховка.
- Ограничьте права токена
GITHUB_TOKEN:permissions: contents: readв workflow, если запись не нужна. - Запомните ключевые правила: раннер всегда крутится не от root. Отдельный пользователь, никаких sudo. И самое главное: секреты — только через Settings → Secrets, никогда не коммитьте их в код.
- Знаете, почему так важна строгая сегрегация сети? У нас железное правило: production-раннеры не имеют права доступа к staging-среде. И, конечно, наоборот — staging-системы не должны «заглядывать» в production. Это ключевой момент для безопасности.
- Ephemeral runners: флаг
--ephemeralпри регистрации. Агент выполнит одну job и самоуничтожится. Самая безопасная схема.
Кейс: стриминговый сервис, 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 МБ на агент плюс потребление самой сборки.
