SSD Nodes Learn 8GB RAM — $66/год
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-01

Self-hosted GitHub Actions runner на VPS

Настройте self-hosted GitHub Actions runner на Ubuntu 24.04: отдельный пользователь, checksum, config.sh, служба systemd и риск pull request из fork.

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

Что делает самостоятельно размещаемый runner GitHub Actions

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

CI (непрерывная интеграция) на принадлежащем вам сервере полезна по двум причинам. Время сборки больше не тарифицируется. Кроме того, задание получает доступ к ресурсам, которые есть только на вашем компьютере, например к прогретому кэшу сборки или частной сети. Цена этого — безопасность. Runner выполняет все действия, указанные в файле workflow, с правами пользователя, от имени которого он запущен. Поэтому файл workflow по своей сути предоставляет удаленное выполнение кода. В частном репозитории это приемлемо, поскольку добавлять такие файлы могут только люди, которым вы доверяете. В общедоступном репозитории это реальный риск. Механизм описан в разделе о pull request из fork.

Все дальнейшие инструкции рассчитаны на Ubuntu 24.04 и runner версии 2.336.0 — текущий выпуск по состоянию на July 2026.

Что необходимо подготовить

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

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

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

Не запускайте runner от имени root или собственной административной учетной записи. Каждое задание получает права пользователя runner, поэтому workflow, вызывающий 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 и проверьте tarball

С этого момента работайте от имени пользователя 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 бит) относится к tarball версии 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

Что содержит tarball и чего в нем нет

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

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

Установите зависимости 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 для входящих в комплект библиотек. Поэтому неразрешенная зависимость останавливает скрипт, а не приводит позже к непонятному сбою.

Зарегистрируйте runner в репозитории

Получите токен в репозитории. Откройте 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 определяет, как runner отображается в репозитории. Выберите имя, которое сможете распознать и через шесть месяцев. --labels добавляет собственные метки. Runner уже получает self-hosted, Linux и X64 автоматически. --work задаёт каталог, в котором размещаются checkout, внутри каталога runner. --unattended отвечает на интерактивные запросы значениями по умолчанию. Это необходимо, когда команда находится в скрипте. --replace заменяет существующую регистрацию с таким же именем, а не завершается с ошибкой. Это необходимо при пересоздании сервера.

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

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

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

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

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

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

Для svc.sh требуются права root, поскольку команда записывает юнит в /etc/systemd/system и включает его. Аргумент после install задает пользователя, от имени которого работает служба. Явно укажите gharunner. Если аргумент не задан, скрипт использует $SUDO_USER. Это ваша учетная запись администратора. В таком случае каждое задание выполняется от имени пользователя, который может использовать sudo.

Имя юнита состоит из имени репозитория и 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. Статус Offline означает, что runner не запущен или не может подключиться к GitHub через порт 443.

Отправка задания исполнителю

runs-on выбирает исполнителя по метке. Укажите self-hosted и свою метку, чтобы задание не попало на исполнителя, которого вы не выбирали.

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 должна существовать на исполнителе. Поэтому одно лишнее слово оставит задание в очереди без ошибок. Сравните список с метками, указанными рядом с исполнителем в настройках репозитория.

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

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

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

Настройки подтверждения лишь частично снижают риск и не устраняют его. Политика по умолчанию для публичного репозитория просит maintainer подтвердить workflow из fork нового участника. После однократного подтверждения последующие pull request этого пользователя выполняются без нового запроса. Поэтому защита сводится к тому, что человек каждый раз читает diff, а payload, скрытый на третьем уровне вызовов build script, легко пропустить.

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

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

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

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

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

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

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

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

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

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

Чтобы удалить runner, сначала удалите службу, затем отмените регистрацию 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 еще не завершил регистрацию. Зарегистрируйте runner, затем установите службу.

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 из каталога runner от имени root, затем зарегистрируйте runner повторно.

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

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

FAQ

Почему в sudo ./svc.sh install отображается сообщение command not found?

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

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

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

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

Да, но GitHub не рекомендует этого делать. Запрос на слияние из fork содержит собственный файл workflow. Поэтому любой пользователь, который может создать fork вашего репозитория, может предложить команды для выполнения на вашей машине. Запрос на подтверждение охватывает только первый запуск участника. Если вы подключаете runner к общедоступному репозиторию, отключите в нем workflow для запросов на слияние из fork, не размещайте на этом сервере ничего другого и регулярно пересоздавайте машину.

Почему регистрация завершается ошибкой 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