SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-10-15

Власний 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.

Що робить self-hosted GitHub Actions runner

Self-hosted GitHub Actions runner — це програма, яку ви встановлюєте на власному VPS. Вона запитує GitHub про доступні завдання та виконує їх на вашому обладнанні. Ви реєструєте runner для одного репозиторію, встановлюєте його як systemd service, і він запускається після кожного перезавантаження. GitHub планує виконання завдання. Ваш сервер виконує роботу.

CI (continuous integration) на власному сервері має сенс із двох причин. Плата за хвилини збірки більше не нараховується, а завдання може отримувати доступ до ресурсів, доступних лише вашій машині, наприклад до підготовленого build cache або приватної мережі. Ціна цього — безпека. Runner виконує все, що вказано у workflow file, від імені користувача, якого ви для нього призначили. Тому workflow file за задумом забезпечує remote code execution. У приватному репозиторії це прийнятно, оскільки додавати workflow file можуть лише люди, яким ви довіряєте. У публічному репозиторії це створює реальний ризик. Механізм описано в розділі про pull requests з fork.

Усе нижче стосується Ubuntu 24.04 і runner версії 2.338.0 — поточного релізу станом на 12 October 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 зберігає там облікові дані у відкритому тексті, а checkout може містити приватний вихідний код.

Перш ніж продовжити, перевірте обидві властивості:

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.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) відповідає tarball версії 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

Що містить 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, а libicu — за допомогою ldconfig перед запуском. Це дозволяє зупинити скрипт через нерозв'язане посилання, замість того щоб спричинити незрозумілий збій пізніше.

Зареєструйте runner у репозиторії

Отримайте токен у репозиторії. Відкрийте 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 визначає, як 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 і public repositories несумісні

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

Механізм простий. Pull request із fork містить власну копію файлу workflow. Якщо ваш public repository запускає workflow для pull request на вашому runner, будь-хто, хто може створити fork репозиторію, може запропонувати workflow, який виконає його команди на вашому VPS. Права на запис не потрібні, оскільки саме запропонований код буде виконано.

Налаштування підтвердження лише частково зменшують ризик. Стандартна політика для public repository просить maintainer підтвердити workflow з fork від автора, який надсилає зміни вперше. GitHub вважає автора новим лише доти, доки жоден його commit або pull request не було об’єднано до репозиторію. Після цього його наступні pull request запускаються без запиту підтвердження. Сам GitHub попереджає, що для цього достатньо незначного виправлення, об’єднаного до репозиторію. Отже, захист зводиться до ручного перегляду diff, а payload, захований на третьому рівні викликів у build script, легко не помітити.

Fork pull request не отримує ваші secrets, а його GITHUB_TOKEN доступний лише для читання. Це обмежує шкоду в межах GitHub. Для вашого сервера це нічого не змінює. Зловмисник отримує shell від імені gharunner, тому може читати кожен файл, доступний цьому користувачеві, звертатися до всього, до чого має доступ VPS у приватній мережі, і залишити шкідливий код у ~/.bashrc або в user systemd unit, який запуститься під час наступного job.

Реєстрація через --ephemeral змушує runner прийняти один job, а потім скасувати реєстрацію. Завдяки цьому один job не може прочитати workspace наступного job. Це допомагає лише тоді, коли для кожного job машину або контейнер перебудовують заново, оскільки backdoor, записаний у home directory користувача runner, зберігається після повторної реєстрації.

Наведені далі правила короткі. Використовуйте self-hosted runners для private repositories. Якщо runner потрібно підключити до public repository, не запускайте на ньому fork pull requests, не розміщуйте на цьому сервері нічого іншого та вважайте машину одноразовою.

Завдання 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 за замовчуванням оновлюється автоматично. Він виявляє новий реліз, замінює власні файли та перезапускає сервіс, тому зазвичай нічого робити не потрібно. Щоб вимкнути автоматичне оновлення, під час реєстрації runner передайте --disableupdate до ./config.sh. Для вже зареєстрованого 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 ще не завершив реєстрацію. Зареєструйте runner, а потім встановіть сервіс.

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 з каталогу runner від імені root, а потім повторіть реєстрацію.

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

Диск заповнюється. Копії репозиторіїв, кеші збірок і Docker images накопичуються в _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 знаходить скрипт і записує 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 для pull request із 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