SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-07

Настройка self-hosted runner для GitHub Actions на VPS

Пошаговое руководство по установке GitHub Actions runner на Ubuntu 24.04. Вы узнаете, как создать службу systemd, настроить права пользователя и избежать рисков безопасности.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Что делает self-hosted GitHub Actions runner

Self-hosted GitHub Actions runner — это программа, которую вы устанавливаете на собственный VPS. Она запрашивает у GitHub задания и выполняет их на вашем оборудовании. Вы регистрируете её для конкретного репозитория, устанавливаете как службу systemd, и она автоматически запускается после каждой перезагрузки. GitHub планирует задание, а ваш сервер выполняет работу.

CI (непрерывная интеграция) на собственном сервере полезна по двум причинам. Время сборки перестает тарифицироваться, а задание получает доступ к ресурсам, которые есть только на вашей машине, например, к кэшу сборки или частной сети. Цена этого — безопасность. Runner выполняет всё, что указано в файле workflow, от имени пользователя, под которым он запущен. Таким образом, файл workflow по своей сути является инструментом для удаленного выполнения кода (RCE). В частном репозитории это допустимо, так как добавлять файлы могут только доверенные лица. В публичном репозитории это создает реальный риск; раздел о fork pull requests объясняет этот механизм.

Всё, что описано ниже, применимо к Ubuntu 24.04 и версии runner 2.336.0, актуальной на июль 2026 года.

Что потребуется перед началом

Начните с VPS, на которой есть обычная учетная запись администратора с правами sudo — это состояние, которого вы достигаете в первые десять минут на новом VPS. Открывать входящие порты не нужно. Runner открывает исходящее HTTPS-соединение к GitHub и удерживает его активным в ожидании задач, поэтому GitHub никогда не подключается к вашему серверу напрямую. Ваш файрвол может оставаться закрытым для внешнего мира, а задания всё равно будут поступать.

Вам также потребуются права администратора в репозитории, так как токен для регистрации отображается в настройках репозитория.

Создание выделенного пользователя для runner

Никогда не запускайте runner от имени root или от имени вашего административного пользователя. Каждое задание наследует права пользователя runner, поэтому рабочий процесс, вызывающий sudo, выполнится успешно, если пользователь runner имеет право использовать sudo. Создайте одного непривилегированного пользователя, который не владеет ничем, кроме собственного домашнего каталога. В Учетные записи с минимальными привилегиями на VPS описана общая схема. Ниже приведен конкретный пример.

sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runner

passwd -l блокирует пароль, поэтому никто не сможет войти в систему как gharunner с его использованием. Права доступа 700 для каталога runner важны, так как runner хранит там свои учетные данные в открытом виде, а выгрузка исходного кода может содержать приватные данные.

Проверьте оба свойства, прежде чем двигаться дальше:

sudo passwd -S gharunner
sudo -l -U gharunner

passwd -S выводит строку, начинающуюся с gharunner L, где L означает, что пароль заблокирован. sudo -l -U gharunner должен ответить is not allowed to run sudo. Если вместо этого выводится список разрешенных команд, значит, учетная запись находится в группе sudo, и созданная вами изоляция нарушена.

Загрузка runner и проверка архива

Далее работайте от имени пользователя runner.

sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.336.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
  "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"

Сначала выполните uname -m, если вы не уверены в архитектуре системы. x86_64 использует файл linux-x64, указанный выше. aarch64 использует actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.

Теперь проверьте загруженный файл. Значение SHA256 (secure hash algorithm, 256 bit), приведенное ниже, относится к архиву версии 2.336.0 для архитектуры x64. GitHub публикует значение для текущего релиза на странице выпуска и на экране New self-hosted runner. Оно меняется с каждой версией, поэтому копируйте его оттуда при установке другого релиза.

echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d  actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -c

Успешная загрузка выводит одну строку:

actions-runner-linux-x64-2.336.0.tar.gz: OK

Поврежденный или измененный файл вызывает ошибку и предупреждение:

actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match

Не пропускайте проверку, чтобы не полагаться на tar при поиске проблем. Неполный архив вызывает ошибки gzip: stdin: unexpected end of file и tar: Unexpected EOF in archive, которые сообщают о повреждении файла, но не уточняют, был ли он обрезан или заменен.

tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
ls

Содержимое архива и его назначение

После распаковки в директории находятся config.sh, run.sh, env.sh, safe_sleep.sh, bin/ и externals/. В bin/ содержатся исполняемые файлы раннера и bin/installdependencies.sh. В externals/ находится встроенная среда выполнения Node, на которой исполняются JavaScript-действия.

Файла svc.sh пока нет. Документация GitHub описывает его как скрипт, «который создается после успешного добавления раннера», так как он генерируется из шаблона, в который вшиты имя вашего репозитория и имя раннера в качестве имени службы. Поэтому попытка запуска sudo ./svc.sh install до ./config.sh завершается ошибкой sudo: ./svc.sh: command not found. Сначала выполните регистрацию, затем установите службу.

Установка зависимостей runner

Runner является приложением на базе .NET, поэтому ему требуются некоторые общие библиотеки. Выйдите из оболочки пользователя runner и установите их с помощью sudo, так как скрипт вносит изменения в системную базу данных пакетов.

exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.sh

В Ubuntu 24.04 это загружает libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 и libicu74. Скрипт перебирает несколько вариантов имен версий для каждой библиотеки и оставляет ту, которую предоставляет ваш дистрибутив; именно поэтому один и тот же скрипт работает как в старых версиях Ubuntu, так и в Debian.

Пропустите этот шаг, и ./config.sh остановится, не выполнив никаких действий:

Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.

Отсутствие libicu приводит к аналогичному результату, но с другой первой строкой — Libicu's dependencies is missing for Dotnet Core 6.0. Оба сообщения имеют один источник: config.sh перед запуском выполняет ldd для проверки связанных библиотек. Таким образом, неразрешенная ссылка останавливает скрипт сразу, предотвращая непредсказуемые сбои в дальнейшем.

Регистрация раннера в репозитории

Получите токен в репозитории. Откройте Settings, затем Actions, затем Runners, затем New self-hosted runner. На странице отобразится токен регистрации, начинающийся с A. Он истекает через один час после создания, поэтому генерируйте его непосредственно перед использованием.

Выполните регистрацию от имени пользователя runner. config.sh не запускается с правами sudo.

sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
  --token PASTE_REGISTRATION_TOKEN_HERE \
  --name vps-runner-1 \
  --labels vps \
  --work _work \
  --unattended \
  --replace

Назначение этих флагов. --name определяет имя, под которым раннер будет виден в репозитории, поэтому выберите название, которое будет понятно вам через полгода. --labels добавляет ваши собственные метки; раннер уже имеет метки self-hosted, Linux и X64 по умолчанию. --work задает имя директории внутри папки раннера, куда будут выгружаться исходные коды. --unattended автоматически выбирает значения по умолчанию для интерактивных запросов, что удобно при запуске команды из скрипта. --replace принудительно перерегистрирует раннер с тем же именем вместо ошибки, что полезно при пересборке сервера.

Успешное завершение работы команды сопровождается выводом следующих строк:

√ Runner successfully added
√ Runner connection is good
√ Settings Saved.

Данные регистрации сохраняются в директории раннера в файлах .runner, .credentials и .credentials_rsaparams. Последние два файла идентифицируют раннер для GitHub, поэтому любой, кто получит к ним доступ, сможет выдать себя за него. Именно по этой причине директория имеет права доступа 700, а у пользователя отсутствуют права sudo.

Установка runner как службы systemd

./run.sh в терминале подходит для разового теста, но процесс завершится вместе с вашей SSH-сессией. Установите службу, чтобы runner запускался при загрузке системы. В systemd services and timers on a VPS описаны сами unit-файлы. Здесь svc.sh создаст такой файл за вас.

exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh status

svc.sh требует прав root, так как записывает unit в /etc/systemd/system и включает его. Аргумент после install — это пользователь, от имени которого работает служба. Укажите gharunner явно. Без аргумента скрипт использует $SUDO_USER, то есть вашу учетную запись администратора, и тогда каждое задание будет выполняться от пользователя, имеющего доступ к sudo.

Unit называется по имени репозитория и runner, в формате actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Вам не нужно вводить это вручную:

systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pager

Исправно работающий runner выводит в лог √ Connected to GitHub, а затем строку, заканчивающуюся на Listening for Jobs, а на странице Runners в репозитории он отображается как Idle. Если runner отображается как Offline, значит, он либо не запущен, либо не может связаться с GitHub по порту 443.

Отправка задания на runner

runs-on выбирает runner по метке. Указывайте self-hosted вместе с вашей собственной меткой, чтобы задание не попало на runner, который вы не планировали использовать.

name: build
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: [self-hosted, linux, vps]
    steps:
      - uses: actions/checkout@v5
      - run: uname -a

Если задание ожидает в Waiting for a runner to pick up this job, значит, метки не совпадают. Каждая метка в runs-on должна присутствовать на runner, поэтому даже одно лишнее слово оставит задание в очереди без вывода ошибок. Сравните список со списком меток, отображаемых рядом с runner в настройках репозитория.

Почему не следует использовать self-hosted runners для публичных репозиториев

Это тот раздел, который многие пропускают. Рекомендации GitHub предельно ясны: self-hosted runners «почти никогда не должны использоваться для публичных репозиториев», так как они «не гарантируют запуск в эфемерных чистых виртуальных машинах и могут быть скомпрометированы недоверенным кодом в рабочем процессе».

Механизм прост. Pull request из форка содержит собственную копию файла рабочего процесса. Если ваш публичный репозиторий запускает рабочие процессы pull request на вашем раннере, любой, кто может сделать форк репозитория, может предложить рабочий процесс, который выполнит его команды на вашем VPS. Им не нужен доступ на запись, так как предлагаемый ими код и есть то, что будет исполнено.

Настройки подтверждения смягчают эту проблему, но не решают её. Политикой по умолчанию для публичного репозитория требуется, чтобы сопровождающий одобрил рабочий процесс из форка от нового участника. После того как вы одобрите этого человека один раз, его последующие pull requests будут выполняться без дополнительного запроса. Таким образом, барьером становится человек, читающий diff каждый раз, а вредоносную нагрузку, скрытую на три уровня глубже в скрипте сборки, легко пропустить.

Pull request из форка не получает доступ к вашим секретам, а его GITHUB_TOKEN доступен только для чтения. Это ограничивает ущерб внутри GitHub. Однако это никак не защищает ваш сервер. У злоумышленника есть оболочка от имени gharunner, поэтому он может прочитать любой файл, доступный этому пользователю, получить доступ ко всему, что видит VPS в своей частной сети, и оставить что-либо в ~/.bashrc или в пользовательском systemd unit, который запустится во время следующего задания.

Регистрация с помощью --ephemeral заставляет раннер принять одно задание, а затем отменить регистрацию, поэтому одно задание не может прочитать рабочую область следующего. Это помогает только в том случае, если что-то пересоздает машину или контейнер для каждого задания, так как бэкдор, записанный в домашний каталог пользователя раннера, сохраняется после повторной регистрации.

Правила ниже кратки. Используйте self-hosted runners для приватных репозиториев. Если вы обязаны подключить такой раннер к публичному репозиторию, не запускайте на нем pull requests из форков, не храните на этом сервере ничего другого и относитесь к этой машине как к одноразовой.

Задания Docker и группа, эквивалентная root

Задания в контейнерах, сервисные контейнеры и любые шаги рабочего процесса, вызывающие docker build, требуют наличия демона Docker на хосте исполнителя (runner). Установите Docker стандартным способом, как описано в Docker и Docker Compose на VPS, а затем добавьте пользователя исполнителя в группу docker.

Осознайте риски перед выполнением этой операции. Членство в группе docker эквивалентно правам root, так как контейнер может выполнить bind mount для / и запуститься внутри с правами root. Таким образом, рабочий процесс, имеющий доступ к сокету Docker, может читать и записывать любые файлы на VPS, включая /etc/shadow. В случае с приватным репозиторием, где работают доверенные участники, это может быть приемлемой ценой. В любых других ситуациях это сводит на нет смысл использования непривилегированного пользователя. Rootless Docker позволяет выполнять сборку контейнеров в рамках прав пользователя-исполнителя, но ценой этого является более медленный драйвер хранилища и невозможность использования привилегированных контейнеров.

Обновление и корректное удаление runner

Self-hosted runner обновляется автоматически по умолчанию. Он обнаруживает новый релиз, заменяет собственные файлы и перезапускает службу, поэтому обычно вам не нужно ничего делать. Флаг ./config.sh --disableupdate отключает автоматическое обновление, если вам нужна фиксированная версия. После этого обновление становится вашей задачей: документация GitHub прямо указывает, что runner, настроенный с помощью --disableupdate, должен обновляться вручную.

Ручное обновление сохраняет регистрацию, так как файлы .runner и .credentials отсутствуют в архиве. Остановите службу, скачайте и проверьте контрольную сумму нового архива как gharunner, распакуйте его в ту же директорию с помощью tar xzf, а затем снова запустите службу:

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start

Чтобы удалить runner, сначала деинсталлируйте службу, а затем выполните дерегистрацию. Токен для удаления можно получить на той же странице Runners, нажав кнопку Remove в настройках конкретного runner.

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HERE

Удаление директории без дерегистрации приведет к тому, что runner останется в списке репозитория со статусом Offline, так как GitHub узнает об удалении только тогда, когда runner сообщит об этом сам или администратор удалит запись вручную.

Типовые ошибки и соответствующие им сообщения

Must not run with sudo. config.sh выводит это сообщение и завершает работу при запуске от имени root. Эта проверка намеренна, так как файлы, принадлежащие root в _work, нарушают работу всех последующих задач, выполняемых от имени пользователя службы. Запускайте ./config.sh от имени gharunner. Переменная RUNNER_ALLOW_RUNASROOT позволяет обойти эту проверку, но её использование лишь откладывает возникновение ошибки на более поздний этап.

sudo: ./svc.sh: command not found. Вы находитесь в нужном каталоге. svc.sh еще не существует, так как config.sh не завершил регистрацию. Зарегистрируйте раннер, а затем установите службу.

Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Токен не является действительным токеном регистрации. Либо он истек (срок их действия составляет один час), либо вместо токена регистрации со страницы Runners был вставлен персональный токен доступа (personal access token). Создайте новый токен и вставьте его снова.

Dependencies is missing for Dotnet Core 6.0. Запустите sudo ./bin/installdependencies.sh из каталога раннера от имени root, а затем выполните регистрацию повторно.

Раннер в статусе Offline после перезагрузки. Запустите systemctl is-enabled 'actions.runner.*'. Если список пуст, значит ./svc.sh install никогда не запускался, и раннер существовал только в рамках вашего сеанса терминала. Если юнит включен, а раннер по-прежнему находится в статусе Offline, изучите journalctl -u 'actions.runner.*' и проверьте исходящий HTTPS-трафик.

Переполнение диска. Клоны репозиториев, кэши сборок и образы Docker накапливаются в _work и в домашнем каталоге пользователя раннера, и система не очищает их автоматически. Мониторьте du -sh /home/gharunner/actions-runner/_work и настройте регулярную очистку, прежде чем диск заполнится полностью.

FAQ

Почему sudo ./svc.sh install выдает ошибку command not found?

Потому что svc.sh отсутствует в tarball-архиве раннера. Файл создается в директории раннера после завершения регистрации ./config.sh, используя имя репозитория и имя раннера для формирования названия службы. Сначала запустите ./config.sh от имени пользователя, под которым работает раннер. После этого sudo ./svc.sh install gharunner обнаружит скрипт и запишет unit-файл с именем actions.runner.OWNER-REPO.RUNNER-NAME.service в /etc/systemd/system.

Нужно ли открывать порт в файрволе для self-hosted раннера?

Нет. Раннер открывает исходящее HTTPS-соединение к GitHub и удерживает его в ожидании заданий, поэтому GitHub никогда не инициирует подключение к вашему VPS. Разрешите исходящий трафик на 443 порт и оставьте входящие правила закрытыми. Если раннер отображается как Offline, хотя служба запущена, проверьте исходящую фильтрацию и настройки DNS, а не входящие правила.

Можно ли использовать self-hosted раннер для публичного репозитория?

Можно, но GitHub не рекомендует этого делать. Pull request из форка содержит собственный файл workflow, поэтому любой, кто может сделать форк вашего репозитория, способен предложить команды, которые будут выполнены на вашей машине. Запрос на подтверждение появляется только при первом запуске для конкретного контрибьютора. Если вы подключаете раннер к публичному репозиторию, отключите на нем выполнение workflow для pull request из форков, не храните на этом сервере ничего другого и регулярно переустанавливайте систему.

Почему регистрация завершается ошибкой Http response code: NotFound?

Вызов регистрации возвращает NotFound не только при неверном URL, но и при неверных учетных данных, что делает сообщение вводящим в заблуждение. Токены регистрации действуют один час после отображения, а personal access token для этого вызова не подходит. Откройте Settings, Actions, Runners, New self-hosted runner еще раз, скопируйте свежий токен и убедитесь, что значение --url указывает на репозиторий, в котором у вас есть права администратора.

#github-actions#ci#self-hosted#runner#ubuntu-24-04