HostiServer
2026-08-13 09:47
GitHub Actions self-hosted runner: встановлення та перший pipeline на VPS
📚 Серія «CI/CD з нуля», частина 3 з 6:
- Що таке CI/CD: від ручного deploy до автоматизованих pipelines
- Self-hosted CI/CD: концепція, архітектура і runner'и
- GitHub Actions self-hosted runner: встановлення та перший pipeline на VPS ← ви тут
- GitLab CI self-hosted runner: встановлення та перший pipeline на VPS
- Gitea Actions: Git-платформа і pipeline на одному сервері
- GitHub Actions vs GitLab CI vs Gitea Actions у 2026: що обрати для self-hosted CI/CD
GitHub Actions self-hosted runner: встановлення та перший pipeline на VPS
У другій частині ми розібрали, як влаштований self-hosted runner: агент забирає завдання з черги платформи, виконує його на вашому сервері і повертає логи та статус. Там же ми дивилися, чому цей агент варто винести на власне залізо і як ізолювати виконання коду. Тепер та сама архітектура перетворюється на конкретні команди: беремо VPS, підключаємо його до репозиторію на GitHub і доводимо ланцюжок до робочого деплою на продакшн.
Ця частина практична від початку до кінця. Спершу коротко про будову GitHub Actions, щоб YAML далі читався осмислено, а не копіювався навмання. Далі встановлення агента, оформлення його як systemd-сервісу, перший workflow, робота з секретами і повний pipeline, який після push у main збирає проєкт і викладає його на сервер через SSH.
1.1 Що вже маємо і що робимо далі
Мінімум, з якого стартуємо:
- репозиторій на GitHub, приватний або внутрішній для організації;
- VPS з Ubuntu 22.04 або 24.04, доступ по SSH, окремий користувач без прав root;
- права адміністратора репозиторію: без них сторінка з налаштуванням runner-ів недоступна.
Агенту не потрібна біла адреса і відкриті вхідні порти. Він сам встановлює з'єднання з GitHub по 443 порту і тримає його, чекаючи на завдання. Це важлива властивість: runner можна поставити всередині закритої мережі, куди ззовні немає доступу взагалі, і він однаково працюватиме.
1.2 GitHub Actions: з чого складається екосистема
GitHub Actions це вбудована в GitHub система автоматизації, запущена у 2019 році. Конфігурація живе у самому репозиторії, у каталозі .github/workflows/: кожен YAML-файл там це окремий workflow, який GitHub підхоплює автоматично, без реєстрації в інтерфейсі.
Друга частина екосистеми це Marketplace готових дій. Дія (action) це перевикористовуваний крок, оформлений як окремий репозиторій: actions/checkout клонує код, actions/setup-node ставить потрібну версію Node.js, docker/build-push-action збирає і публікує образ. Замість десятка рядків bash у конфізі з'являється один рядок uses.
Зручність має зворотний бік. Кожна стороння дія це чужий код, який виконується у вашому оточенні і бачить ваші секрети. На self-hosted runner-і ціна помилки вища, ніж на одноразовій хмарній машині: у вас цей сервер живе далі. Робоче правило це фіксувати дії за повним хешем коміта замість тега, принаймні для всього, що не належить організаціям actions і github:
# замість рухомого тега
- uses: some-org/some-action@v3
# фіксація на конкретний коміт
- uses: some-org/some-action@8f4b7e2c9a1d3f5b6c8e0a2d4f6b8c0e2a4d6f8b # v3.1.0
ℹ️ Термінологія: у GitHub Actions немає стадій як окремої сутності, які є у GitLab CI. Порядок виконання задається залежностями через ключ needs. Якщо ви прийшли з GitLab, це головна відмінність, до якої треба звикнути.
2. Архітектура GitHub Actions
Перед встановленням агента корисно розібрати три рівні, на яких описується робота. Далі весь YAML у статті спирається саме на них.
2.1 Workflow → Job → Step
Workflow це один YAML-файл у .github/workflows/. Він відповідає на питання «коли запускатися» і «що робити». Файлів може бути скільки завгодно: окремо перевірки на pull request, окремо нічне сканування залежностей, окремо реліз за тегом.
Job це задача всередині workflow. Кожна задача отримує власний runner і власне чисте оточення. Задачі за замовчуванням виконуються паралельно, а послідовність задається через needs. Важливий наслідок: дві задачі не бачать файлів одна одної, обмін іде через артефакти.
Step це окремий крок усередині задачі. Кроки виконуються послідовно в одному оточенні, тому файли між ними передаються звичайним чином. Крок буває двох типів: run для команд оболонки і uses для готової дії.
workflow (файл ci.yml)
├── job: build → runner №1, власне оточення
│ ├── step: checkout
│ ├── step: npm ci
│ └── step: npm run build
└── job: deploy → runner №2, чисте оточення
needs: build
├── step: download-artifact
└── step: ssh deploy
2.2 Тригери: коли запускається workflow
Тригери описуються у секції on. Чотири основні:
- push у певну гілку або за тегом. Базовий тригер для CI і для релізів.
- pull_request. Перевіряє результат злиття ще до того, як воно сталося. Саме на цей тригер вішають блокування злиття, поки перевірки не пройдуть.
- schedule. Запуск за розкладом у синтаксисі cron, у часовому поясі UTC. Підходить для нічних прогонів і щоденного сканування залежностей.
- workflow_dispatch. Запуск кнопкою в інтерфейсі або викликом через API, з опційними параметрами. Це той самий ручний крок, який відрізняє Continuous Delivery від Continuous Deployment.
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [main]
schedule:
- cron: '0 3 * * *' # щодня о 03:00 UTC
workflow_dispatch:
inputs:
environment:
description: 'Куди викладати'
type: choice
options: [staging, production]
default: staging
Про розклад є нюанс, який економить години розслідувань: schedule працює лише для workflow у гілці за замовчуванням, а запуск може затриматися на кілька хвилин або зсунутися під пікове навантаження. Для точного часу потрібен зовнішній планувальник, який смикає workflow_dispatch через API.
2.3 GitHub-hosted і self-hosted runner: різниця на практиці
GitHub-hosted runner це віртуальна машина, яку GitHub створює під кожну задачу і знищує після неї. Self-hosted runner це ваш сервер з встановленим агентом, який живе постійно. Різниця виходить далеко за межі того, хто платить за залізо.
| Параметр | GitHub-hosted | Self-hosted |
|---|---|---|
| Життя оточення | Чиста ВМ під кожну задачу | Постійне, стан накопичується |
| Ресурси | Фіксовані за тарифом | Ваші, аж до GPU і сотень ГБ RAM |
| Оплата | За хвилини збірки | За сервер, хвилини не рахуються |
| Передвстановлений софт | Великий образ з мовами і утилітами | Тільки те, що поставили ви |
| Доступ до закритої мережі | Немає | Є, runner стоїть усередині |
| Черга у пікові години | Можлива | Своя черга, передбачуваний час |
| Адміністрування | Немає | Оновлення, диск, ізоляція, моніторинг |
Рядок про постійне оточення це і головна перевага, і головна пастка. Перевага у швидкості: між збірками лишається кеш пакетів, шари Docker, склонований репозиторій, тому друга збірка часто вдвічі швидша за першу. Пастка у тому, що там лишається і все інше: глобально встановлені пакети, тимчасові файли, залишені змінні оточення, приватний ключ, який попередня задача записала у домашній каталог і забула прибрати.
⚠️ Публічні репозиторії: GitHub прямо не рекомендує підключати self-hosted runner до публічного репозиторію. Будь-хто може відкрити pull request, а разом з ним і запропонувати зміни у workflow або у коді, який цей workflow виконує. На одноразовій хмарній машині це закінчується разом із задачею. На вашому сервері чужий код отримує доступ до всього, що на ньому лежить, і до мережі, у якій він стоїть. Для open source лишайте GitHub-hosted runner-и.
3. Встановлення self-hosted runner на VPS
Далі йде послідовність, після якої агент з'явиться у списку runner-ів зі статусом Idle і переживатиме перезавантаження сервера.
3.1 Реєстрація в GitHub і отримання токена
Runner підключається на одному з трьох рівнів:
- Репозиторій. Settings → Actions → Runners → New self-hosted runner. Найпростіший варіант для першого агента.
- Організація. Той самий шлях у налаштуваннях організації. Один агент обслуговує кілька репозиторіїв, доступ розмежовується групами runner-ів.
- Enterprise. Рівень для великих інсталяцій, логіка та сама.
Сторінка одразу показує готові команди під обрану ОС і архітектуру, з актуальною версією агента і контрольною сумою. Копіюйте їх звідти: версія оновлюється регулярно, і команди з чужої статті швидко застарівають.
⚠️ Токен реєстрації живе близько години. Якщо між копіюванням і запуском config.sh минуло більше часу, конфігурація впаде з помилкою авторизації. Рішення просте: оновити сторінку і взяти новий токен. Цей токен потрібен лише для підключення агента, для роботи він не використовується.
3.2 Завантаження і запуск агента
Усі команди виконуються від звичайного користувача, не від root. Агент відмовиться налаштовуватися під root, і це правильна поведінка: задачі виконуються з правами того користувача, від якого запущено агента.
# окремий системний користувач під runner
sudo adduser --disabled-password --gecos "" runner
sudo usermod -aG docker runner # лише якщо збірки використовують Docker
sudo su - runner
mkdir -p ~/actions-runner && cd ~/actions-runner
# версію і посилання беремо зі сторінки New self-hosted runner
RUNNER_VERSION=2.3xx.x
curl -o actions-runner.tar.gz -L \
https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz
# звірка контрольної суми з тією, що показав GitHub
echo "<хеш зі сторінки> actions-runner.tar.gz" | shasum -a 256 -c
tar xzf ./actions-runner.tar.gz
Розпакований каталог містить config.sh для підключення, run.sh для запуску, svc.sh для оформлення сервісу і скрипт bin/installdependencies.sh, який доставляє системні бібліотеки, потрібні .NET-середовищу агента. Останній запускається один раз і потребує sudo.
sudo ./bin/installdependencies.sh
./config.sh \
--url https://github.com/OWNER/REPO \
--token <токен зі сторінки> \
--name vps-build-01 \
--labels self-hosted,linux,x64,build \
--work _work \
--unattended \
--replace
Що означають ключі:
--nameце ім'я у списку runner-ів. Робіть його осмисленим: коли агентів стане п'ять,vps-build-01читається краще заubuntu-server.--labelsце мітки, за якими workflow обирає агента. До трьох стандартних (self-hosted, ОС, архітектура) додавайте власні:build,deploy,gpu.--unattendedприбирає інтерактивні питання, що потрібно для автоматизації через Ansible або cloud-init.--replaceперезаписує агента з тим самим іменем, якщо він уже зареєстрований.
Перевірити зв'язок можна одразу, у передньому плані:
./run.sh
# √ Connected to GitHub
# Listening for Jobs
У цей момент агент з'являється у списку зі статусом Idle. Зупиняємо його по Ctrl+C і переходимо до сервісу.
3.3 Runner як systemd-сервіс
Запуск через run.sh живе рівно до закриття сесії. Для постійної роботи агент оформлюється сервісом, і скрипт для цього вже лежить у каталозі:
sudo ./svc.sh install runner
sudo ./svc.sh start
sudo ./svc.sh status
Скрипт створює unit з іменем виду actions.runner.OWNER-REPO.vps-build-01.service і вмикає автозапуск. Далі з ним працюють звичайними засобами systemd:
systemctl status 'actions.runner.*'
journalctl -u 'actions.runner.*' -f
sudo ./svc.sh stop
sudo ./svc.sh uninstall
Детальні логи виконання задач лежать окремо, у каталозі _diag усередині папки агента. Туди варто заглядати, коли задача падає без зрозумілого повідомлення в інтерфейсі GitHub.
Два налаштування, які краще зробити одразу:
- Ротація робочого каталогу. Каталог
_workросте нескінченно: кожен репозиторій, кожен артефакт, кожен кеш. Раз на тиждень чистіть його за розкладом або стежте за вільним місцем, бо закінчення диска виглядає як хаотичні падіння збірок. - Автооновлення. Агент оновлюється сам, коли GitHub випускає нову версію. Якщо оновлення має відбуватися лише у вашому вікні обслуговування, додайте
--disableupdateпри конфігурації і оновлюйте вручну. Врахуйте, що застарілий агент з часом перестає приймати задачі.
4. Перший workflow
Агент підключений, тепер даємо йому роботу.
4.1 Структура YAML-файлу
Файл кладеться у .github/workflows/ci.yml і має три обов'язкові частини: name для відображення в інтерфейсі, on для тригерів, jobs для самої роботи.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
GitHub підхоплює файл автоматично після push. Реєструвати його ніде не потрібно, а помилки синтаксису видно на вкладці Actions одразу після коміта.
4.2 runs-on: як задача знаходить ваш агент
Ключ runs-on це фільтр за мітками. Задача піде на агента, у якого є всі перелічені мітки:
# будь-який ваш агент
runs-on: self-hosted
# лише linux x64 серед ваших
runs-on: [self-hosted, linux, x64]
# лише агент з міткою gpu
runs-on: [self-hosted, linux, gpu]
# хмарний агент GitHub
runs-on: ubuntu-latest
Якщо агента з потрібним набором міток немає або він офлайн, задача не падає, а стає у чергу і чекає. За замовчуванням вона висітиме там кілька годин, після чого скасується за тайм-аутом. Це типова причина ситуації «pipeline запустився і нічого не відбувається»: перевіряйте не логи, а збіг міток і статус агента.
Мітки зручно комбінувати з групами runner-ів на рівні організації: група визначає, які репозиторії взагалі мають право слати задачі на цей агент, а мітки вже розподіляють задачі всередині дозволеного.
4.3 Робочий приклад: checkout, залежності, тести
Повний конфіг з коментарями до кожного блоку:
name: CI
on:
push:
branches: [main]
pull_request:
# нові запуски для тієї самої гілки скасовують попередні
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: [self-hosted, linux, x64]
timeout-minutes: 20
steps:
- name: Отримати код
uses: actions/checkout@v4
- name: Поставити Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Встановити залежності
run: npm ci
- name: Лінтер
run: npm run lint
- name: Тести
run: npm test -- --ci
- name: Зберегти звіт
if: always()
uses: actions/upload-artifact@v4
with:
name: test-report
path: reports/
retention-days: 7
Три деталі, які на self-hosted runner-і важливіші, ніж на хмарному:
timeout-minutes. Без нього зависла задача займає агента на шість годин, і всі наступні збірки стоять у черзі. На своєму агенті це блокує всю команду, бо запасних машин немає.concurrency. Скасовує попередні запуски тієї самої гілки. Коли агент один, це прибирає безглузді прогони для комітів, які вже перекриті новішими.if: always(). Крок виконається навіть після падіння попередніх. Для збереження звітів це обов'язково, бо цікавий саме звіт про невдалий прогін.
ℹ️ Про setup-node і подібні дії: на self-hosted runner-і вони кешують встановлені версії у каталозі агента, тому перший запуск довший, а наступні майже миттєві. Альтернатива це поставити потрібні версії у систему заздалегідь, але тоді оновлення версії перестає бути зміною одного рядка у конфізі і повертається у ручні операції на сервері.
5. Secrets та змінні
Pipeline майже завжди потребує того, чого не можна класти у репозиторій: SSH-ключів, токенів реєстру образів, паролів до бази. GitHub має для цього окреме сховище.
5.1 Де зберігаються секрети
Шлях в інтерфейсі: Settings → Secrets and variables → Actions. Там дві вкладки. У Secrets лежать значення, які маскуються у логах і не читаються назад після збереження. У Variables лежать звичайні налаштування без маскування: назва гілки, адреса staging-сервера, версія образу.
Секрети існують на трьох рівнях:
| Рівень | Хто бачить | Коли обирати |
|---|---|---|
| Repository | Усі workflow цього репозиторію | Значення, потрібне лише цьому проєкту |
| Organization | Обрані репозиторії організації | Спільний токен реєстру, ключ моніторингу |
| Environment | Лише задачі з відповідним environment |
Продакшн-доступи, які треба відділити від решти |
Маскування працює на рівні тексту: якщо значення секрету потрапляє у вивід, GitHub замінює його на зірочки. Захист не абсолютний. Секрет, розібраний на частини, закодований у base64 або виведений посимвольно, у логах видно повністю. Тому окрім маскування діє звичайне правило мінімальних прав: ключ, який може лише покласти файли у каталог і перезапустити один сервіс, не стане катастрофою навіть після витоку.
5.2 Environment secrets для staging і production
Оточення (Settings → Environments) це найкорисніший механізм у цьому розділі. Воно робить дві речі одночасно: тримає власний набір секретів і накладає правила на задачі, які до нього звертаються.
- Required reviewers. Задача зупиняється і чекає на підтвердження від конкретної людини. Це і є ручна кнопка Continuous Delivery, але з іменем відповідального у логах.
- Deployment branches. Обмеження, з яких гілок можна викладати. Ставте
mainдля production, і випадковий деплой з гілки з експериментом стане неможливим. - Wait timer. Пауза перед виконанням, під час якої деплой можна скасувати.
jobs:
deploy-staging:
runs-on: [self-hosted, linux, deploy]
environment: staging
steps:
- run: ./scripts/deploy.sh
env:
HOST: ${{ secrets.DEPLOY_HOST }} # значення з оточення staging
deploy-production:
needs: deploy-staging
runs-on: [self-hosted, linux, deploy]
environment: production # чекає на підтвердження рецензента
steps:
- run: ./scripts/deploy.sh
env:
HOST: ${{ secrets.DEPLOY_HOST }} # те саме ім'я, інше значення
Однакове ім'я секрету у двох оточеннях це не збіг, а зручність: workflow лишається одним, а різницю між середовищами тримає GitHub.
5.3 Використання секретів у workflow
Звертання відбувається через контекст secrets:
steps:
- name: Правильно: через env
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
run: ./scripts/publish.sh
- name: Небезпечно: значення потрапляє у командний рядок
run: ./scripts/publish.sh --token ${{ secrets.API_TOKEN }}
Різниця між двома кроками не косметична. У другому випадку значення підставляється у рядок команди ще до запуску оболонки, тому воно видно у ps на сервері і потрапляє в історію оболонки. Через env воно передається змінною оточення процесу, і це помітно вужча поверхня.
⚠️ Pull request із форків: для таких запусків секрети недоступні за замовчуванням, і це захист від очевидної атаки. Тригер pull_request_target цей захист знімає, бо виконує workflow у контексті базової гілки разом з секретами. Використовуйте його тільки з кодом базової гілки і ніколи не робіть у ньому checkout коду з форка.
6. Практичний приклад: deploy на сервер через SSH
Збираємо все разом. Мета: після push у main проєкт збирається на runner-і, артефакт їде на продакшн-сервер, сервіс перезапускається.
6.1 Ключ для деплою
Ключ генерується окремий, тільки під цю задачу, і ніколи не збігається з особистим ключем інженера:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./gh_deploy -N ""
На продакшн-сервері створюємо користувача без прав root і додаємо йому публічну частину з обмеженнями:
# на продакшн-сервері
sudo adduser --disabled-password --gecos "" deploy
sudo install -o deploy -g deploy -m 700 -d /home/deploy/.ssh
# /home/deploy/.ssh/authorized_keys
from="203.0.113.10",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAA... github-actions-deploy
Параметр from прив'язує ключ до адреси вашого runner-а: навіть з викраденим приватним ключем зайти з іншої машини не вийде. Права на перезапуск сервісу видаються точково, через sudoers, а не загальним доступом до sudo:
# /etc/sudoers.d/deploy
deploy ALL=(root) NOPASSWD: /bin/systemctl restart app.service
6.2 Секрети у репозиторії
Додаємо три значення в оточення production:
SSH_PRIVATE_KEYце вміст файлуgh_deployповністю, разом з рядкамиBEGINіENDта кінцевим переносом рядка.SSH_KNOWN_HOSTSце вивідssh-keyscan -H your-server.example.com.DEPLOY_HOSTце адреса сервера.
Другий пункт часто пропускають і замінюють на StrictHostKeyChecking=no. Так робити не варто: ця опція вимикає перевірку, яка захищає від підміни сервера, а її наявність у конфізі означає, що deploy поїде куди завгодно, аби там відповідали по SSH.
6.3 Повний workflow
name: Build and deploy
on:
push:
branches: [main]
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
build:
runs-on: [self-hosted, linux, x64, build]
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test -- --ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
deploy:
needs: build
runs-on: [self-hosted, linux, deploy]
environment: production
timeout-minutes: 10
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Підготувати SSH
run: |
install -m 700 -d ~/.ssh
install -m 600 /dev/null ~/.ssh/deploy_key
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Викласти файли
run: |
rsync -az --delete \
-e "ssh -i ~/.ssh/deploy_key" \
dist/ deploy@${{ secrets.DEPLOY_HOST }}:/var/www/app/
- name: Перезапустити сервіс
run: |
ssh -i ~/.ssh/deploy_key deploy@${{ secrets.DEPLOY_HOST }} \
"sudo systemctl restart app.service"
- name: Перевірити, що піднявся
run: |
for i in $(seq 1 10); do
code=$(curl -s -o /dev/null -w "%{http_code}" https://app.example.com/health || true)
[ "$code" = "200" ] && exit 0
sleep 3
done
echo "Сервіс не відповідає після деплою"
exit 1
- name: Прибрати ключ
if: always()
run: rm -f ~/.ssh/deploy_key
Останній крок з if: always() це не формальність, а прямий наслідок постійного оточення. На хмарному runner-і машина зникає разом із ключем. Ваш агент живе далі, і приватний ключ у домашньому каталозі лишиться доступним будь-якій наступній задачі, включно з тією, що прийшла з іншої гілки.
Крок з перевіркою здоров'я перетворює деплой на дію з результатом. Без нього workflow зеленіє після рестарту сервісу, навіть якщо застосунок одразу впав у краш-цикл, і про аварію ви дізнаєтеся від клієнтів.
ℹ️ Коли SSH не потрібен взагалі: якщо runner стоїть на тому самому сервері, що й застосунок, деплой зводиться до копіювання файлів локально. Спокуса зрозуміла, але збірка це навантаження на CPU, диск і пам'ять, і воно конкуруватиме з продакшном у найгірший момент. Робочий компроміс це окрема машина під runner і приватна мережа між нею і продакшном: збірка ізольована, а трафік деплою не виходить у публічний інтернет.
7. Висновок
Self-hosted runner для GitHub Actions встановлюється за кілька команд, і основна робота починається після них. Що варто винести з цієї частини:
- Агент не потребує відкритих портів. Він сам іде до GitHub по 443 порту, тому спокійно живе у закритій мережі поруч з продакшном.
- Мітки це механізм маршрутизації.
runs-onшукає агента з усіма переліченими мітками, і задача, що висить у черзі, майже завжди означає розбіжність саме тут. - Постійне оточення прискорює і накопичує. Кеш між збірками це виграш, залишені файли і ключі це ризик. Прибирання після себе стає частиною workflow.
- Оточення важливіші за окремі секрети. Environment з рецензентами і обмеженням гілок дає і розділення доступів, і ручний крок перед продакшном.
- Деплой закінчується перевіркою. Зелений pipeline без health-check означає лише те, що команди виконалися, а не те, що застосунок працює.
Мінімальний робочий набір виглядає так: окремий користувач під агента, systemd-сервіс, мітки під типи задач, timeout-minutes у кожній задачі, секрети в оточенні production і прибирання ключів у кроці з if: always().
Що далі у серії
У четвертій частині ми зробимо те саме на GitLab CI: встановимо GitLab Runner на VPS, розберемо executors (shell, docker, docker+machine) і пройдемо шлях від першого .gitlab-ci.yml до деплою на продакшн. Порівняння з GitHub Actions буде наскрізним, бо архітектурні рішення там відрізняються сильніше, ніж синтаксис.
📚 Навігація по серії:
Ви читаєте частину 3 з 6 «GitHub Actions self-hosted runner: встановлення та перший pipeline на VPS».
Попередня: ← Частина 2. Self-hosted CI/CD: концепція, архітектура і runner'и
Наступна: Частина 4. GitLab CI self-hosted runner: встановлення та перший pipeline на VPS →
🚀 VPS і виділені сервери під ваші GitHub Actions runner'и
Runner упирається у CPU під час збірки, у диск під час встановлення залежностей і у мережу під час деплою. Hostiserver дає для цього передбачувані ресурси і мережу, з якої можна безпечно ходити на продакшн.
🖥️ Виділені Сервери
- Від $90/міс, повний контроль над залізом для важких збірок і паралельних runner-ів
- NVMe-накопичувачі: швидкий кеш npm, Maven і Docker-шарів між прогонами
- Без обмеження хвилин збірки: платите за сервер, а не за час pipeline
- Приватна мережа між runner-ом і продакшн-серверами
- 24/7 підтримка: інженери допоможуть з налаштуванням агента і деплою
💻 Cloud (VPS) Хостинг
- Від $19.95/міс, KVM-ізоляція, виділені vCPU і RAM
- Ідеально для першого runner-а: підняти, підключити до репозиторію, перевірити на реальному проєкті
- Легко масштабувати: окремий VPS під build і окремий під deploy, з різними мітками
💬 Не впевнені, який варіант вам необхідний?
💬 Напишіть нам і ми зі всім допоможемо!
Часті питання
- Чи можна поставити runner на той самий сервер, де живе продакшн?
Технічно так, і для невеликого проєкту це працює. Проблема у конкуренції за ресурси: збірка з'їдає CPU і пам'ять саме тоді, коли викладається реліз, тобто у момент найбільшого навантаження на застосунок. Друга проблема це права: агент, що стоїть поруч з продакшном, зазвичай отримує доступ до файлів застосунку і до перезапуску сервісів, а разом з ним цей доступ отримує будь-який код, який виконується у pipeline. Компроміс це окремий дешевий VPS під runner і приватна мережа до продакшну.
- Скільки ресурсів потрібно VPS під один runner?
Сам агент споживає близько 150-250 МБ RAM у простої, решта залежить від збірок. Для типового веб-проєкту на Node.js або Python вистачає 2 vCPU і 4 ГБ RAM, для збірок з Docker краще 4 vCPU і 8 ГБ. Диск важливіший, ніж здається: 40 ГБ це мінімум, бо
_work, кеш пакетів і Docker-шари ростуть швидко. Один агент виконує одну задачу за раз, тому для паралельних збірок або додають ще агентів, або беруть сервер потужніший.
- Runner показує статус Offline, хоча сервіс запущений. Що перевіряти?
Спершу логи:
journalctl -u 'actions.runner.*' -n 100і каталог_diagу папці агента. Найчастіші причини це блокування вихідних з'єднань на 443 порт до github.com з боку фаєрвола або корпоративного проксі, застаріла версія агента, яку GitHub уже не приймає, і закінчене місце на диску. Окремий випадок це видалений runner на боці GitHub: агент продовжує працювати, але його токен більше не дійсний, і допомагає лише повторна конфігурація з новим токеном.
- Чи безпечно вмикати self-hosted runner у публічному репозиторії?
Ні, і GitHub про це прямо попереджає. Будь-хто може відкрити pull request, і його код виконається на вашому сервері. Оточення при цьому постійне, тому наслідки не зникають після завершення задачі: залишений процес, змінений системний пакет, прочитані файли з робочого каталогу. Для публічних проєктів беріть GitHub-hosted runner-и. Якщо власний агент потрібен саме там, лишається варіант з ephemeral-агентами у одноразових контейнерах і обов'язковим підтвердженням запуску для зовнішніх контриб'юторів.
- Як запустити кілька runner-ів на одному сервері?
Кожен агент живе у власному каталозі з власною конфігурацією. Створюєте
~/actions-runner-1,~/actions-runner-2, у кожному запускаєтеconfig.shз унікальним--name, потімsvc.sh install. Вийде два незалежні systemd-сервіси і дві паралельні задачі. Розумна межа це кількість ядер, поділена на два: агенти конкурують за CPU і диск, і чотири одночасні збірки на двоядерному VPS будуть повільнішими, ніж дві послідовні.
- Чи потрібен actions/cache, якщо оточення і так постійне?
Здебільшого ні. На постійному агенті
node_modules, каталог~/.m2або~/.cache/pipлишаються між прогонами, томуactions/cacheдодає зайвий цикл вивантаження і завантаження через мережу GitHub. Виняток це кілька агентів: тоді спільний кеш вирівнює час збірки незалежно від того, кому дісталася задача. Для ephemeral-агентів кеш потрібен обов'язково, бо оточення щоразу нове.
- Що робити, коли на runner-і немає потрібної версії мови або утиліти?
Два шляхи. Перший це дії на кшталт
setup-node,setup-python,setup-java: вони самі завантажують потрібну версію і кешують її на агенті, а версія лишається описаною у репозиторії. Другий це виконувати кроки у контейнері через ключcontainerна рівні задачі, і тоді оточення повністю описується образом. Ручне встановлення пакетів на сервері працює теж, але повертає ту саму проблему, з якої ми починали серію: оточення відоме лише тому, хто його налаштовував.