Власний GitHub Actions runner на VPS: налаштування
Налаштуйте self-hosted GitHub Actions runner на Ubuntu 24.04: окремий користувач, checksum, config.sh, systemd і ризик pull request із fork.
Як працює власний 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-runnerpasswd -l блокує пароль, тому ніхто не може ввійти як gharunner за допомогою пароля. Режим 700 для каталогу runner важливий, оскільки runner зберігає там облікові дані у відкритому тексті, а checkout може містити приватний вихідний код.
Перш ніж продовжити, перевірте обидві властивості:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -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 (безпечний алгоритм хешування, 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 від імені користувача 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, тому будь-хто, хто може їх прочитати, може видати себе за нього. Саме тому для каталогу встановлено режим 700, а користувач не має доступу до sudo.
Встановлення runner як служби systemd
./run.sh у терміналі підходить для одного тесту, але процес завершується разом із вашим сеансом SSH. Встановіть службу, щоб runner запускався під час завантаження системи. У матеріалі служби та таймери systemd на VPS описано самі unit-файли. Тут svc.sh створить unit-файл замість вас.
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. Статус 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 або в user 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 вимикає автоматичне оновлення, коли потрібна фіксована версія. Після цього оновлювати runner потрібно вручну: у документації GitHub прямо зазначено, що runner, налаштований із --disableupdate, потрібно оновлювати вручну.
Під час ручного оновлення реєстрація зберігається, оскільки .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, оскільки GitHub дізнається про його видалення лише тоді, коли runner повідомить про це або адміністратор вручну видалить запис.
Типові причини помилок і повідомлення, які ви побачите
Must not run with sudo. config.sh виводить це повідомлення та завершує роботу, якщо його запущено від імені root. Ця перевірка виконується навмисно, оскільки файли в _work, власником яких є root, порушують роботу всіх наступних завдань, що виконуються від імені користувача служби. Запускайте ./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 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 відсутній у tarball runner. Він створюється в каталозі runner після завершення реєстрації через ./config.sh. Для формування імені сервісу використовуються назви вашого репозиторію та runner. Спочатку виконайте ./config.sh від імені користувача runner. Після цього sudo ./svc.sh install gharunner знаходить скрипт і записує блок з назвою 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 вашого репозиторію, може запропонувати команди, які виконуватимуться на вашій машині. Запит на підтвердження стосується лише першого запуску від конкретного учасника. Якщо ви підключаєте runner до публічного репозиторію, вимкніть у ньому workflow для pull request із fork, не розміщуйте на цьому сервері нічого іншого та регулярно перебудовуйте машину.
Чому реєстрація завершується помилкою Http response code: NotFound?
Під час неправильних облікових даних запит реєстрації повертає NotFound, а не лише тоді, коли неправильний URL. Через це повідомлення може вводити в оману. Токени реєстрації стають недійсними через одну годину після відображення. Personal access token для цього запиту не приймається. Знову відкрийте Settings, Actions, Runners, New self-hosted runner, скопіюйте новий токен і переконайтеся, що значення --url вказує на репозиторій, у якому ви маєте права адміністратора.