SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Настройка 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), от имени пользователя, под которым он запущен. Таким образом, файл рабочего процесса по своей сути является механизмом удаленного выполнения кода. В закрытом репозитории это допустимо, так как добавлять файлы могут только доверенные лица. В публичном репозитории это создает реальный риск; раздел о fork pull requests объясняет этот механизм.

Всё, что описано ниже, применимо к Ubuntu 24.04 и версии runner 2.338.0 — это актуальный релиз на 12 октября 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.338.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.338.0 под архитектуру x64. GitHub публикует значение для текущего релиза на странице выпуска и на экране New self-hosted runner. Оно меняется с каждой версией, поэтому копируйте его оттуда при установке другого релиза.

echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32  actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -c

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

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

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

actions-runner-linux-x64-2.338.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.338.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, а libicu — с помощью ldconfig перед запуском. Таким образом, неразрешенная ссылка останавливает скрипт, предотвращая непредсказуемый сбой в дальнейшем.

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

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

Выполните регистрацию от имени пользователя 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. Ему не нужны права на запись, так как исполняется именно то, что он предлагает.

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

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

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

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

Задания 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 обновляется автоматически по умолчанию. Он обнаруживает новый релиз, заменяет собственные файлы и перезапускает службу, поэтому обычно вам не нужно ничего предпринимать. Чтобы отключить автоматическое обновление, передайте --disableupdate в ./config.sh при регистрации runner; для уже зарегистрированного runner это означает повторную регистрацию с данным флагом. Это не позволяет оставаться на одной версии бесконечно. Документация GitHub прямо указывает, что runner, настроенный с --disableupdate, всё равно должен быть обновлен вручную в течение 30 дней после выхода нового релиза, иначе GitHub перестанет ставить задачи в очередь для него.

Ручное обновление сохраняет регистрацию, так как .runner и .credentials отсутствуют в tarball. Остановите службу, скачайте и проверьте контрольную сумму нового tarball как 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. Он останется в списке до тех пор, пока администратор не нажмет Force remove this runner, или пока GitHub не удалит его автоматически через 14 дней отсутствия соединения.

Типичные ошибки и сообщения о них

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). Сгенерируйте новый токен, вставьте его снова и убедитесь, что --url содержит полный URL репозитория.

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

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

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

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

Можно, но GitHub не рекомендует этого делать. Pull request из fork содержит собственный файл workflow, поэтому любой, кто может сделать fork вашего репозитория, способен предложить команды, которые будут выполнены на вашей машине. Стандартный запрос на подтверждение распространяется только на тех участников, чьи коммиты или pull request еще не были приняты в репозиторий. Если вы подключаете runner к публичному репозиторию, отключите на нем выполнение workflow для fork 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