bolt Valebyte VPS від $4/міс — NVMe, запуск за 60 секунд.

Отримати VPS arrow_forward
eco Початковий Туторіал

CI/CD на власному VPS: self-hosted runner для GitHub Actions

calendar_month Sep 16, 2026 schedule 24 хв. читання visibility 65 переглядів
CI/CD на своём VPS: GitHub Actions self-hosted runner
info

Потрібен сервер для цього гайду? Ми пропонуємо виділені сервери та VPS у 50+ країнах з миттєвим налаштуванням.

Потрібен сервер для цього гайду?

Розгорніть VPS або виділений сервер за хвилини.

CI/CD на власному VPS: GitHub Actions self-hosted runner

TL;DR

GitHub Actions self-hosted runner дає змогу виконувати CI/CD-завдання GitHub Actions на власному VPS: збирати Docker-образи, запускати тести, деплоїти застосунки та працювати з ресурсами, недоступними в GitHub-hosted runners. У цьому гайді буде налаштовано безпечний Linux-runner на Ubuntu 24.04 LTS з Docker, systemd, firewall, автоматичними оновленнями та резервним копіюванням конфігурації.

  • Runner працює як агент: він сам підключається до GitHub через HTTPS, тому відкривати для нього вхідні порти не потрібно.
  • Для приватного репозиторію зазвичай достатньо VPS з 2 vCPU, 4 ГБ RAM і 40–60 ГБ SSD.
  • Docker дає змогу запускати контейнерні job, збирати образи та використовувати Docker Compose у workflow.
  • Self-hosted runner не можна бездумно підключати до публічних репозиторіїв із pull request від зовнішніх користувачів.
  • Runner запускається через systemd і автоматично стартує після перезавантаження VPS.
  • Для production-деплою краще використовувати окремий runner, окремого Linux-користувача та обмежені GitHub Secrets.

Що ми налаштовуємо і навіщо

Схема: Что мы настраиваем и зачем
Схема: Що ми налаштовуємо і навіщо

GitHub Actions — вбудована CI/CD-система GitHub. Зазвичай workflow запускаються на інфраструктурі GitHub: для кожного job створюється тимчасова віртуальна машина з Linux, Windows або macOS. Це зручно, але такий підхід має обмеження: ліміти часу, вартість хвилин, відсутність доступу до внутрішньої мережі, фіксована продуктивність і неможливість використовувати специфічне обладнання.

Self-hosted runner — це агент, установлений на вашому VPS, dedicated-сервері або машині у внутрішній мережі. Коли workflow у репозиторії містить умову runs-on: self-hosted, GitHub надсилає завдання на відповідний runner. Runner завантажує вихідний код, виконує команди workflow, надсилає логи назад до GitHub і чекає на наступне завдання.

У результаті ви отримаєте Linux-сервер, який можна використовувати для типових сценаріїв CI/CD:

  • збирання Docker-образів і публікація в GitHub Container Registry, Docker Hub або приватний registry;
  • тестування Node.js, Python, Go, PHP, Java, Rust та інших проєктів;
  • запуск лінтерів, статичного аналізу та інтеграційних тестів;
  • збирання фронтенду та підготовка release-артефактів;
  • деплой через SSH, Docker Compose, Ansible або API хмарного провайдера;
  • доступ до приватних PostgreSQL, Redis, VPN-мережі або внутрішнього API;
  • використання кешів залежностей між збірками;
  • виконання ресурсоємних завдань без оплати кожної хвилини GitHub-hosted runner.

Як працює self-hosted runner

На VPS не потрібно відкривати вебінтерфейс, приймати webhook або публікувати API. Агент сам встановлює вихідне TLS-з'єднання з інфраструктурою GitHub через порт 443/tcp. Тому runner можна розмістити за NAT, у приватній мережі або на сервері із суворим вхідним firewall.

Після реєстрації GitHub видає runner унікальні облікові дані. Вони зберігаються локально в каталозі runner. Під час запуску агент авторизується, отримує список доступних завдань і зіставляє свої labels із вимогами workflow. Наприклад, job з runs-on: [self-hosted, linux, x64, docker] не буде надіслано на runner без мітки docker.

GitHub-hosted runner чи self-hosted runner

Критерій GitHub-hosted runner Self-hosted runner на VPS
Обслуговування GitHub оновлює базове середовище Ви відповідаєте за ОС, Docker, патчі та безпеку
Вартість Хвилини Actions, особливо помітно в приватних репозиторіях Фіксована вартість VPS і трафіку
Швидкість Залежить від вибраного тарифу GitHub Залежить від CPU, RAM, NVMe та кешів вашого сервера
Мережа Немає доступу до вашої приватної мережі без додаткових рішень Можна підключити VPN, private network і локальні сервіси
Ізоляція Нова тимчасова VM для більшості job Стан диска зберігається, потрібна власна ізоляція
Docker Доступний, але середовище одноразове Можна використовувати локальний registry, BuildKit і постійний кеш

Для невеликого приватного репозиторію GitHub-hosted runner часто простіший: не потрібно адмініструвати сервер. Self-hosted runner виправданий, якщо збірки відбуваються регулярно, потрібні Docker-кеші, доступ до закритої мережі, власний фіксований IP, більше дискового простору або контроль над оточенням.

Головне обмеження безпеки

Workflow — це код. Будь-яка команда з файлу .github/workflows/.yml виконується на runner з правами користувача, під яким працює агент. Якщо цьому користувачу дозволено Docker, то фактично workflow може отримати привілеї рівня root на VPS через Docker daemon.

Не підключайте постійний self-hosted runner з доступом до секретів, production-мережі або Docker daemon до публічного репозиторію, де зовнішні користувачі можуть створювати pull request. Для недовіреного коду використовуйте GitHub-hosted runners, ізольовані ephemeral runners або окремі одноразові віртуальні машини.

У цьому посібнику мається на увазі приватний репозиторій або команда з контрольованим доступом на запис. Для production-деплою рекомендовано створити окремий runner з label production, який запускає лише захищені workflow із захищеної гілки та GitHub Environment.

Яка VPS-конфігурація потрібна для цього завдання

Схема: Какой VPS-конфиг нужен под эту задачу
Схема: Яка VPS-конфігурація потрібна для цього завдання

Ресурси GitHub Actions self-hosted runner визначаються не самим агентом, а вашими job. Порожній runner споживає мало пам'яті та майже не використовує CPU. Однак Docker build, тести, компіляція TypeScript, Java, Rust або Android здатні швидко зайняти всю доступну RAM, процесор і диск.

Сценарій CPU RAM Диск Підходить для
Мінімальний 2 vCPU 4 ГБ 40 ГБ SSD Лінтери, unit-тести, невеликий Node.js/Python-проєкт
Робочий універсальний 4 vCPU 8 ГБ 80–120 ГБ NVMe Docker Compose, кілька сервісів, регулярні збірки
Важкий CI 8 vCPU 16 ГБ 160–250 ГБ NVMe Java, Rust, monorepo, паралельні job, великі образи
Збирання Android або великих образів 8–16 vCPU 32 ГБ 300 ГБ+ NVMe Gradle, емулятори, multi-platform build, великі артефакти

Для першого runner під приватний репозиторій розумна стартова конфігурація — 4 vCPU, 8 ГБ RAM, 100 ГБ NVMe та канал від 100 Мбіт/с. Такий сервер витримає Docker-збирання API, фронтенду та базові інтеграційні тести. За потреби можна взяти VPS із зазначеними характеристиками або підібрати конфігурацію в іншого провайдера зі швидким NVMe-диском.

Чому важливий обсяг диска

Docker не видаляє невикористовувані образи, build cache, зупинені контейнери та volumes автоматично. У CI диск зазвичай закінчується раніше за CPU. Один образ із кількома шарами може займати 1–3 ГБ, а кеш залежностей Node.js, Maven, Gradle або Cargo — ще десятки гігабайтів.

Залишайте щонайменше 20–30% вільного місця. Для Docker корисно регулярно виконувати docker system prune, але не запускайте агресивне очищення під час build: воно може видалити потрібні образи або кеш активного завдання.

Коли потрібен dedicated, а не VPS

VPS достатньо для більшості проєктів з одним runner і послідовними job. Dedicated-сервер варто обирати, коли потрібна гарантована продуктивність CPU та диска, одночасно працюють кілька runner, збираються важкі Android-проєкти, великі Docker-образи або виконуються ресурсоємні тести.

Також dedicated виправданий, якщо CI обробляє конфіденційний код, ключі підпису релізів, великі бази тестових даних або постійно виконує високе навантаження. На виділеному сервері простіше передбачити продуктивність і зменшити вплив сусідніх віртуальних машин на storage I/O.

Локація VPS

Локація впливає насамперед на затримку до GitHub, Docker Registry, package registry і цільових серверів деплою. Якщо runner збирає образи та надсилає їх у registry в Європі, європейська локація зазвичай зменшить час завантаження. Якщо runner деплоїть сервіс на сервер у конкретному регіоні, розміщуйте його ближче до production-інфраструктури.

Для звичайних тестів різниця в географії менш критична, ніж швидкість NVMe та стабільність каналу. Але якщо workflow часто завантажує залежності обсягом кілька гігабайтів, перевіряйте включений трафік і пропускну здатність порту.

Підготовка сервера

Схема: Підготовка сервера
Схема: Підготовка сервера

Далі використовується Ubuntu Server 24.04 LTS x86_64. На момент налаштування у 2026 році це стабільна LTS-гілка з підтримкою безпеки. Команди виконуються від імені початкового адміністратора з правами root або sudo. Замінюйте значення в кутових дужках на свої, але не вводьте самі символи < і >.

Оновіть операційну систему

Спочатку встановіть усі доступні патчі. Після оновлення ядра VPS знадобиться перезавантаження. Перевірити необхідність можна за файлом /var/run/reboot-required.

# Обновляет индекс пакетов и устанавливает актуальные обновления Ubuntu.
sudo apt update && sudo apt full-upgrade -y

# Удаляет больше не нужные зависимости и очищает локальный кэш APT.
sudo apt autoremove --purge -y && sudo apt clean

# Перезагружает сервер, если обновилось ядро или системные библиотеки.
sudo reboot

Створіть окремого адміністратора

Не використовуйте root для постійної роботи. Створіть користувача, який виконуватиме адміністративні завдання. Runner пізніше отримає окремий обліковий запис без інтерактивного пароля.

# Создаёт пользователя deployadmin с домашним каталогом и запросом пароля.
sudo adduser deployadmin

# Добавляет пользователя в группу sudo для административных команд.
sudo usermod -aG sudo deployadmin

# Проверяет, что пользователь получил нужные группы.
id deployadmin

Додайте свій публічний SSH-ключ до файлу /home/deployadmin/.ssh/authorized_keys. Якщо ви вже підключені як root і ваш ключ є в /root/.ssh/authorized_keys, його можна скопіювати.

# Создаёт каталог SSH и задаёт безопасные права доступа.
sudo install -d -m 700 -o deployadmin -g deployadmin /home/deployadmin/.ssh

# Копирует существующие авторизованные ключи root новому администратору.
sudo cp /root/.ssh/authorized_keys /home/deployadmin/.ssh/authorized_keys

# Выставляет владельца и права файла с публичными ключами.
sudo chown deployadmin:deployadmin /home/deployadmin/.ssh/authorized_keys
sudo chmod 600 /home/deployadmin/.ssh/authorized_keys

Відкрийте другу SSH-сесію та переконайтеся, що можете увійти як deployadmin за ключем. Лише після цього вимикайте root-login і парольну автентифікацію. Помилка на цьому етапі може позбавити доступу до VPS.

Посильте налаштування SSH

Створіть окремий конфігураційний файл. Ubuntu OpenSSH читає файли з каталогу /etc/ssh/sshd_config.d/, тому не потрібно редагувати основний файл і конфліктувати з оновленнями пакета.

# Создаёт отдельную конфигурацию SSH: root и вход по паролю будут отключены.
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
LoginGraceTime 30
EOF

# Проверяет синтаксис конфигурации до перезапуска SSH.
sudo sshd -t

# Применяет безопасные параметры SSH без остановки активных сессий.
sudo systemctl reload ssh

Встановіть базові утиліти, firewall і Fail2ban

GitHub runner не потребує вхідного порту. Для адміністрування залиште лише SSH. Якщо ви змінювали SSH-порт, дозвольте в UFW саме його. Перед увімкненням firewall перевірте, що поточний IP не блокується зовнішнім мережевим firewall провайдера.

# Устанавливает инструменты диагностики, firewall, Fail2ban и средства работы с архивами.
sudo apt install -y curl wget ca-certificates gnupg lsb-release jq unzip \
  git htop tmux ufw fail2ban ncdu

# Запрещает входящие соединения и разрешает исходящие.
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Разрешает SSH для удалённого администрирования.
sudo ufw allow OpenSSH

# Включает firewall и показывает действующие правила.
sudo ufw --force enable
sudo ufw status verbose

Fail2ban відстежує логи SSH і тимчасово банить IP-адреси з повторними невдалими спробами входу. Він не замінює SSH-ключі та firewall, але зменшує шум від автоматичних сканерів.

# Включает Fail2ban при загрузке и запускает его немедленно.
sudo systemctl enable --now fail2ban

# Показывает состояние SSH-jail и число заблокированных адресов.
sudo fail2ban-client status sshd

Увімкніть автоматичні оновлення безпеки

Автоматичні security-updates корисні для базового захисту VPS. Однак оновлення Docker або ядра може вимагати перезапуску сервісів. Для критичного production-runner краще вибрати фіксоване maintenance window і тестувати оновлення на окремій машині.

# Устанавливает механизм автоматического применения обновлений безопасности Ubuntu.
sudo apt install -y unattended-upgrades

# Запускает интерактивную настройку автоматических security-updates.
sudo dpkg-reconfigure --priority=low unattended-upgrades

Встановлення ПЗ — покроково

Схема: Встановлення ПЗ — покроково
Схема: Встановлення ПЗ — покроково

Для self-hosted runner буде встановлено Docker Engine з офіційного репозиторію Docker і GitHub Actions Runner з офіційних GitHub Releases. Не використовуйте пакет docker.io зі стандартного Ubuntu-репозиторію, якщо вам потрібна передбачувана свіжа версія Docker Engine: офіційний репозиторій Docker зазвичай оновлюється швидше.

Встановіть Docker Engine

У 2026 році орієнтуйтеся на актуальну стабільну гілку Docker Engine 27.x або новішу, доступну для Ubuntu 24.04. Команди нижче додають офіційний ключ і репозиторій, після чого APT встановить актуальний stable-пакет.

# Создаёт каталог для ключей репозиториев APT.
sudo install -m 0755 -d /etc/apt/keyrings

# Загружает и сохраняет официальный GPG-ключ Docker.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# Разрешает APT читать ключ Docker всем пользователям.
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Добавляет официальный stable-репозиторий Docker для текущей архитектуры Ubuntu.
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Обновляет индекс пакетов после добавления нового репозитория.
sudo apt update
# Устанавливает Docker Engine, CLI, Buildx и Docker Compose v2.
sudo apt install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

# Включает Docker при загрузке и запускает daemon сейчас.
sudo systemctl enable --now docker

# Проверяет версии Docker Engine, Compose и Buildx.
docker --version
docker compose version
docker buildx version

Поки команда docker ps від звичайного користувача видаватиме помилку доступу — це нормально. Runner запускатиметься від окремого користувача ghrunner, якого ми свідомо додамо до групи docker.

Членство в групі docker еквівалентне широким root-привілеям на сервері. Не додавайте туди звичайних користувачів і не дозволяйте недовіреним workflow виконуватися на цьому runner.

Створіть системного користувача runner

Для агента не потрібні пароль та інтерактивний SSH-вхід. Окремий системний користувач спрощує аудит файлів, логів і дозволів. Домашнім каталогом буде /opt/actions-runner.

# Создаёт системную учётную запись ghrunner без пароля и с домашним каталогом runner.
sudo useradd --system --create-home --home-dir /opt/actions-runner \
  --shell /usr/sbin/nologin ghrunner

# Даёт runner доступ к Docker daemon для контейнерных workflow.
sudo usermod -aG docker ghrunner

# Проверяет UID, домашний каталог и группы нового пользователя.
id ghrunner
getent passwd ghrunner

Завантажте актуальний GitHub Actions Runner

GitHub регулярно випускає оновлення runner 2.x. Замість жорстко зафіксованого номера команда нижче отримує останню стабільну версію для архітектури x64 через офіційний GitHub API. Це зменшує ризик встановити застарілий агент. Для ARM64 замініть фільтр linux-x64 на linux-arm64.

# Получает URL последнего официального релиза runner для Linux x64.
RUNNER_URL=$(curl -fsSL https://api.github.com/repos/actions/runner/releases/latest \
  | jq -r '.assets[] | select(.name | test("actions-runner-linux-x64-[0-9.]+\\.tar\\.gz$")) | .browser_download_url')

# Показывает найденный URL; он не должен быть пустым.
echo "$RUNNER_URL"

# Скачивает архив runner в домашний каталог системного пользователя.
sudo -u ghrunner curl -fL "$RUNNER_URL" -o /opt/actions-runner/actions-runner.tar.gz

# Распаковывает runner и удаляет архив после успешной распаковки.
sudo -u ghrunner tar xzf /opt/actions-runner/actions-runner.tar.gz \
  -C /opt/actions-runner
sudo -u ghrunner rm /opt/actions-runner/actions-runner.tar.gz

У релізах GitHub зазвичай публікуються контрольні суми. Для середовища з підвищеними вимогами завантажте файл checksums з тієї самої сторінки release і перевірте SHA-256 перед розпакуванням. Це особливо важливо, якщо VPS розташований у недовіреній мережі або пакет завантажується через корпоративний proxy.

Встановіть залежності runner

Скрипт installdependencies.sh встановлює системні бібліотеки, необхідні агенту. Запускайте його через sudo, потім переконайтеся, що каталог runner належить ghrunner.

# Устанавливает системные библиотеки, требуемые GitHub Actions Runner.
cd /opt/actions-runner
sudo ./bin/installdependencies.sh

# Возвращает владение рабочими файлами агенту после установки зависимостей.
sudo chown -R ghrunner:ghrunner /opt/actions-runner

# Показывает версию установленного runner.
sudo -u ghrunner /opt/actions-runner/bin/Runner.Listener --version

Перевірте Docker від імені runner

Ця перевірка критична. Якщо Docker працює в адміністратора, але недоступний користувачу ghrunner, workflow з контейнерами та docker build завершаться помилкою permission denied.

# Проверяет, что пользователь runner видит Docker daemon и может запустить тестовый контейнер.
sudo -u ghrunner docker run --rm hello-world

# Показывает место, занятое образами, контейнерами, volumes и build cache.
sudo docker system df

Налаштування GitHub Actions self-hosted runner

Схема: Налаштування GitHub Actions self-hosted runner
Схема: Налаштування GitHub Actions self-hosted runner

Реєстрація runner виконується з інтерфейсу GitHub. Для runner на рівні репозиторію відкрийте репозиторій, потім перейдіть до Settings → Actions → Runners → New self-hosted runner. Виберіть Linux і x64. GitHub покаже одноразовий registration token і команду налаштування.

Для кількох репозиторіїв зручніше створити runner на рівні організації: Organization Settings → Actions → Runners. Але organisation-level runner отримує завдання від усіх дозволених репозиторіїв, тому використовуйте runner groups і labels, щоб обмежити доступ.

Зареєструйте runner з labels

Токен реєстрації діє обмежений час, зазвичай близько години. Не зберігайте його в нотатках, shell history, Git-репозиторії або файлі конфігурації. Підставте URL свого репозиторію та токен, отриманий у GitHub.

# Переходит в каталог GitHub Actions Runner.
cd /opt/actions-runner

# Регистрирует runner для private-репозитория с понятным именем и labels.
sudo -u ghrunner ./config.sh \
  --url https://github.com/ORG/REPOSITORY \
  --token YOUR_ONE_TIME_REGISTRATION_TOKEN \
  --name ci-vps-01 \
  --labels linux,x64,docker,ci-vps \
  --work _work \
  --unattended \
  --replace

Параметр --replace корисний під час повторної реєстрації runner з тим самим ім'ям. Каталог _work містить checkout репозиторіїв і тимчасові дані job. Він може швидко зростати, тому саме його потрібно враховувати під час планування диска й обслуговування.

Після команди в GitHub поруч із runner має з'явитися статус Idle. Поки агент не встановлено як сервіс, він може бути Offline після завершення процесу. Для постійної роботи зареєструйте systemd-сервіс.

# Устанавливает systemd unit, который будет запускать runner после перезагрузки.
cd /opt/actions-runner
sudo ./svc.sh install ghrunner

# Включает сервис в автозагрузку и запускает его сейчас.
sudo ./svc.sh start

# Проверяет, что systemd видит активный сервис runner.
sudo systemctl status actions.runner..service --no-pager

Ім'я systemd unit містить власника та ім'я репозиторію або організації. Точне ім'я залежить від URL реєстрації. Для перегляду журналів у реальному часі використовуйте journalctl.

# Показывает последние 100 строк лога и продолжает вывод в реальном времени.
sudo journalctl -u 'actions.runner.' -n 100 -f

# Показывает все сервисы GitHub Actions Runner на текущем сервере.
systemctl list-units --type=service --all | grep actions.runner

Приклад workflow для перевірки

Створіть файл .github/workflows/self-hosted-check.yml у вашому репозиторії. Він запускається вручну, виводить характеристики VPS, перевіряє Docker і збирає невеликий контейнер. У workflow немає секретів, тому його безпечно використовувати як початкову діагностику.

name: Self-hosted runner check

on:
  workflow_dispatch:

jobs:
  diagnostics:
    runs-on: [self-hosted, linux, x64, docker, ci-vps]
    timeout-minutes: 20

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Show runner information
        run: |
          whoami
          hostnamectl
          uname -a
          nproc
          free -h
          df -h /

      - name: Check Docker
        run: |
          docker --version
          docker compose version
          docker ps
          docker run --rm hello-world

      - name: Build test image
        run: |
          printf 'FROM alpine:3.21\nCMD ["echo", "CI works"]\n' > Dockerfile.ci-test
          docker build -t ci-test:${{ github.run_id }} -f Dockerfile.ci-test .
          docker run --rm ci-test:${{ github.run_id }}

Зробіть commit, відкрийте вкладку Actions, виберіть workflow і натисніть Run workflow. У журналах має з'явитися інформація про сервер, версія Docker і рядок Hello from Docker!. Після завершення job у GitHub runner повернеться до статусу Idle.

Приклад Docker CI/CD workflow

Нижче наведено приклад, який збирає образ і публікує його в GitHub Container Registry. Змінна GITHUB_TOKEN надається GitHub автоматично. Для публікації потрібні мінімальні permissions contents: read і packages: write. Запуск обмежено гілкою main.

name: Build and publish image

on:
  push:
    branches: [main]

permissions:
  contents: read
  packages: write

jobs:
  build:
    runs-on: [self-hosted, linux, x64, docker, ci-vps]
    timeout-minutes: 30

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/my-app:latest

Секрети, environment variables і production-деплой

Не записуйте паролі, API-ключі, SSH-ключі, registration token або токени registry у YAML-файли. Зберігайте їх у Settings → Secrets and variables → Actions. Для production використовуйте GitHub Environments: створіть оточення production, додайте approval rules і прив'яжіть до нього секрети. Тоді секрети надаються лише job, які явно використовують це оточення.

name: Deploy production

on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: [self-hosted, linux, x64, production]
    environment: production
    timeout-minutes: 20

    steps:
      - uses: actions/checkout@v4

      - name: Deploy through protected SSH key
        env:
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
          DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          install -m 700 -d ~/.ssh
          printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
          ssh "$DEPLOY_USER@$DEPLOY_HOST" 'cd /srv/my-app && docker compose pull && docker compose up -d'

Якщо конкретному застосунку дійсно потрібен файл .env на VPS, зберігайте його поза Git-репозиторієм, наприклад у /etc/my-app/my-app.env з правами 600. Не плутайте цей файл з оточенням runner: GitHub Actions Secrets передаються в job через змінні оточення та зазвичай не потребують постійного зберігання на VPS.

# Создаёт закрытый каталог для локальных production-конфигураций приложения.
sudo install -d -m 700 -o root -g root /etc/my-app

# Создаёт .env-файл, доступный только root; секреты вводятся интерактивно в редакторе.
sudoedit /etc/my-app/my-app.env

# Проверяет владельца и режим доступа, не печатая содержимое секретов.
sudo stat -c '%U:%G %a %n' /etc/my-app/my-app.env

TLS/HTTPS і мережевий доступ

Для GitHub Actions runner Caddy і Certbot не потрібні: агент не публікує HTTP-сервіс і використовує вихідний HTTPS до GitHub. Не відкривайте порти 80 і 443 «для runner» — це не покращує роботу CI/CD та збільшує поверхню атаки.

Перевірте DNS, вихідний TLS і GitHub API наступними командами. Якщо VPS перебуває за корпоративним proxy, налаштуйте змінні HTTPS_PROXY, HTTP_PROXY і NO_PROXY у systemd override для runner, а не у workflow із секретами.

# Проверяет, что VPS может установить TLS-соединение с GitHub API.
curl -I --connect-timeout 10 https://api.github.com

# Проверяет, что GitHub Actions endpoint доступен по HTTPS.
curl -I --connect-timeout 10 https://github.com

# Проверяет DNS-разрешение домена GitHub.
getent hosts github.com

Обмежте паралелізм і очищайте робочу директорію

Один runner виконує лише один job одночасно. Це зазвичай правильно для VPS з обмеженими ресурсами. Якщо потрібна паралельність, створіть другий runner в окремому каталозі й лише після вимірювань збільшуйте CPU, RAM і диск. Не запускайте кілька важких Docker build на сервері з 4 ГБ RAM.

Після job GitHub Actions залишає checkout у _work. Для приватних проєктів це прискорює повторні завдання, але може зберігати тимчасові файли. Наприкінці workflow видаляйте особливо чутливі артефакти, а періодично очищайте старі каталоги після перевірки, що немає активних job.

# Показывает размер рабочей директории и Docker-хранилища.
sudo du -sh /opt/actions-runner/_work 2>/dev/null
sudo docker system df

# Удаляет только неиспользуемые Docker-образы, контейнеры, сети и build cache.
sudo docker system prune -af

Резервні копії та обслуговування

Схема: Резервні копії та обслуговування
Схема: Резервні копії та обслуговування

GitHub Actions runner не містить основної бізнес-бази даних: вихідний код зберігається в GitHub, workflow — у репозиторії, а GitHub Secrets не вивантажуються на VPS як постійна конфігурація. Тому бекап runner простіший, ніж бекап GitLab або Jenkins. Однак потрібно зберегти документацію, локальні конфігурації, scripts деплою, systemd overrides і дані, створені вашими workflow.

Що потрібно і не потрібно бекапити

Дані Бекапити Причина
Репозиторій GitHub і workflow Зазвичай не окремо Вони вже зберігаються в GitHub, але дзеркала корисні для критичних проєктів
/opt/actions-runner/.runner і credentials Ні Містять реєстраційні облікові дані; runner простіше зареєструвати повторно
Скрипти деплою поза Git Так Це локальна операційна конфігурація
Systemd override і конфіги proxy Так Потрібні для швидкого відновлення середовища
/etc/my-app/.env Так, у зашифрованому вигляді Містять production-секрети застосунку
Docker build cache і тимчасові образи Ні Займають багато місця та відновлюються під час наступної збірки
Volumes production-застосунку Так Можуть містити БД, uploads, черги або дані користувачів

Резервне копіювання через restic у S3

Restic створює дедупліковані зашифровані бекапи. Як сховище можна використовувати S3-compatible storage, окремий backup VPS через SFTP або інший віддалений backend. Нижче використовується S3. Не зберігайте пароль репозиторію restic у shell history і не додавайте його до Git.

# Встановлює restic з репозиторію Ubuntu.
sudo apt install -y restic

# Створює закритий каталог для змінних резервного копіювання.
sudo install -d -m 700 -o root -g root /etc/restic

# Створює файл середовища з S3-обліковими даними та паролем шифрування.
sudoedit /etc/restic/runner-backup.env

# Обмежує доступ до файлу із секретами лише користувачем root.
sudo chmod 600 /etc/restic/runner-backup.env

Файл /etc/restic/runner-backup.env повинен мати такий вигляд. Використовуйте окремого S3-користувача з доступом лише до backup bucket. Пароль RESTIC_PASSWORD має бути довгим і також зберігатися в менеджері паролів: без нього відновити дані неможливо.

export RESTIC_REPOSITORY="s3:https://s3.example.net/ci-runner-backup"
export AWS_ACCESS_KEY_ID="CHANGE_ME"
export AWS_SECRET_ACCESS_KEY="CHANGE_ME"
export RESTIC_PASSWORD="CHANGE_ME_TO_A_LONG_UNIQUE_PASSWORD"

Ініціалізуйте порожній репозиторій один раз. Потім створіть скрипт, який архівує лише потрібні каталоги та виключає registration credentials runner.

# Ініціалізує новий зашифрований restic-репозиторій у віддаленому S3-сховищі.
sudo bash -c 'source /etc/restic/runner-backup.env && restic init'

# Створює сценарій щоденного резервного копіювання конфігурацій і локальних даних.
sudo tee /usr/local/sbin/backup-ci-runner.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

source /etc/restic/runner-backup.env

restic backup \
  /etc/systemd/system \
  /etc/my-app \
  /usr/local/sbin \
  /opt/actions-runner \
  --exclude=/opt/actions-runner/_work \
  --exclude=/opt/actions-runner/.credentials \
  --exclude=/opt/actions-runner/.credentials_rsaparams \
  --exclude=/opt/actions-runner/.runner \
  --tag ci-runner

restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
restic check
EOF

# Робить скрипт доступним для запуску лише root.
sudo chmod 700 /usr/local/sbin/backup-ci-runner.sh

Перед додаванням у cron виконайте резервне копіювання вручну та перевірте знімки. Перший запуск може бути тривалим, наступні передаватимуть лише змінені блоки.

# Створює перший backup і одночасно перевіряє доступність віддаленого сховища.
sudo /usr/local/sbin/backup-ci-runner.sh

# Показує список створених snapshots без розкриття вмісту секретів.
sudo bash -c 'source /etc/restic/runner-backup.env && restic snapshots'

# Відкриває root-crontab для додавання розкладу.
sudo crontab -e

Додайте в root-crontab рядок для щоденного запуску о 03:20. Лог корисний під час розслідування помилок, але не має бути єдиним індикатором: налаштуйте зовнішній моніторинг або сповіщення про невдалий backup.

# Запускає backup щодня о 03:20 і записує stdout/stderr в окремий лог.
20 3    /usr/local/sbin/backup-ci-runner.sh >> /var/log/backup-ci-runner.log 2>&1

Перевірка відновлення

Бекап без тесту відновлення — лише припущення. Раз на квартал відновлюйте snapshot у тимчасовий каталог на окремій машині або в каталог під /root/restore-test. Перевіряйте наявність конфігів, права доступу та можливість використовувати відновлені deployment scripts.

# Створює тимчасовий каталог для перевірки відновлення.
sudo mkdir -p /root/restore-test

# Відновлює останній snapshot у тестовий шлях, не зачіпаючи робочу систему.
sudo bash -c 'source /etc/restic/runner-backup.env && restic restore latest --target /root/restore-test'

# Виводить перші рівні відновленого дерева для швидкої перевірки.
sudo find /root/restore-test -maxdepth 3 -type f | head -50

Оновлення runner і Docker

GitHub Actions Runner зазвичай уміє виконувати самооновлення між job. Не вимикайте цей механізм без причини: старі версії можуть перестати отримувати завдання після завершення підтримки. Перед ручним оновленням дочекайтеся, поки runner стане Idle, потім зупиніть сервіс, оновіть файли та поверніть власника каталогу.

Для одиночного runner обирайте maintenance window: тимчасово вимкніть workflow або дочекайтеся завершення job. Це безпечніше, ніж оновлювати Docker daemon посеред збірки. Для кількох runner використовуйте rolling-оновлення: виведіть один runner із runner group, оновіть і перевірте, потім переходьте до наступного.

# Показує поточний статус runner перед плановим обслуговуванням.
sudo systemctl status 'actions.runner.' --no-pager

# Встановлює доступні оновлення ОС і Docker через APT.
sudo apt update && sudo apt upgrade -y

# Перевіряє версії runner і Docker після обслуговування.
sudo -u ghrunner /opt/actions-runner/bin/Runner.Listener --version
docker --version

Регулярно перевіряйте вільне місце, активні контейнери та помилки сервісу. Мінімальний щотижневий набір перевірок: df -h, docker system df, systemctl status, journalctl і статус runner у GitHub.

Усунення несправностей + FAQ

Чому runner відображається як Offline у GitHub?

Спочатку перевірте systemd: sudo systemctl status 'actions.runner.'. Потім перегляньте журнал через sudo journalctl -u 'actions.runner.' -n 100 --no-pager. Часті причини: сервіс не встановлено після реєстрації, VPS не має вихідного доступу на 443/tcp, неправильно налаштований proxy або системний час сильно відрізняється від реального. Перевірте timedatectl і curl -I https://api.github.com. Якщо credentials пошкоджено, видаліть runner в інтерфейсі GitHub і зареєструйте його повторно з новим одноразовим токеном.

Workflow зависає в черзі та не призначається на runner. Що перевірити?

Порівняйте labels у workflow і labels, показані в розділі GitHub Actions Runners. Job з runs-on: [self-hosted, linux, docker] чекатиме нескінченно, якщо runner не має хоча б однієї з цих міток або зайнятий іншим завданням. Також перевірте, чи дозволений runner для цього репозиторію, чи не перебуває він в іншій runner group і чи не увімкнені обмеження організації. Для діагностики тимчасово використовуйте runs-on: self-hosted, а потім поверніть точні labels.

Чому Docker у workflow видає permission denied під час підключення до /var/run/docker.sock?

Runner запущено користувачем ghrunner, якому потрібен доступ до групи docker. Виконайте id ghrunner: у виводі має бути група docker. Якщо її немає, додайте користувача командою sudo usermod -aG docker ghrunner, потім перезапустіть сервіс runner. Перевірте sudo -u ghrunner docker ps. Не виправляйте проблему командою chmod 666 /var/run/docker.sock: це небезпечно і зазвичай тимчасово.

На VPS закінчилося місце під час docker build. Як звільнити диск?

Спочатку визначте джерело: df -h, sudo docker system df -v і sudo du -sh /opt/actions-runner/_work. Після завершення всіх job видаліть невикористовувані Docker-дані через sudo docker system prune -af. Якщо volumes не потрібні, можна додати --volumes, але це видалить невикористовувані томи та може пошкодити локальний застосунок. Для постійного навантаження збільште диск, обмежте розмір артефактів і винесіть registry або кеш в окреме сховище.

Чи можна використовувати self-hosted runner для public repository?

Технічно можна, але постійний runner із Docker і секретами не можна надавати недовіреному коду. У public repository зовнішні учасники можуть відкрити pull request із workflow або кодом, який спробує прочитати файли, викрасти токени, змінити Docker-образи або закріпитися на сервері. Для публічних pull request використовуйте GitHub-hosted runner. Якщо self-hosted обов’язковий, створюйте ephemeral runner для кожної job, ізолюйте його в окремій VM і не надавайте йому доступу до production-мережі, Docker socket і чутливих секретів.

Яка VPS-конфігурація мінімально підійде?

Мінімум для невеликого private-проєкту — 2 vCPU, 4 ГБ RAM, 40 ГБ SSD і стабільний вихідний доступ до інтернету. Такої конфігурації достатньо для лінтингу, unit-тестів і легких Node.js або Python-збірок. Якщо workflow використовує Docker Compose, PostgreSQL, Redis і збірку образу, практичніше одразу обрати 4 vCPU, 8 ГБ RAM і 80–100 ГБ NVMe. Стежте за docker system df: розмір диска в CI важливіший, ніж здається під час першого запуску.

Що обрати — VPS чи dedicated для цього завдання?

Для одного runner і звичайного вебпроєкту обирайте VPS: він дешевший, швидше розгортається та легко масштабується зміною тарифу. Dedicated потрібен за постійних важких збірок, кількох паралельних runner, вимог до стабільного CPU і NVMe I/O, роботи з великими базами й артефактами або підвищених вимог до ізоляції. Почніть із VPS і виміряйте тривалість job, споживання RAM, навантаження CPU та вільне місце. Перехід на dedicated має сенс, коли ресурси VPS стають систематичним вузьким місцем.

Чи потрібно відкривати 80, 443 або налаштовувати Caddy для GitHub Actions Runner?

Ні. Runner сам створює вихідні HTTPS-з’єднання з GitHub, тому вхідні HTTP/HTTPS-порти йому не потрібні. Залиште закритими 80 і 443, якщо на VPS немає окремого вебсервісу. Caddy або Certbot потрібні лише коли цей самий сервер публікує застосунок, registry, dashboard або інший HTTP-сервіс. Для runner достатньо вихідного 443/tcp, коректного DNS і синхронізованого часу. Такий підхід безпечніший: сервер не має зайвої публічної мережевої поверхні.

Чому job після завершення залишає файли й контейнери?

На відміну від GitHub-hosted VM, self-hosted runner є постійним. GitHub очищає деякі службові дані job, але Docker-образи, build cache, volumes, частина checkout і тимчасові файли можуть залишатися на диску. Це прискорює повторні збірки, але потребує обслуговування. Додайте cleanup-step у workflow для чутливих даних, установіть ліміти на артефакти та запускайте планове очищення Docker у вільне вікно. Не видаляйте робочий каталог, поки є активні job: це призведе до непередбачуваних помилок збірки.

Висновки та наступні кроки

Схема: Висновки та наступні кроки
Схема: Висновки та наступні кроки

Тепер на VPS працює GitHub Actions self-hosted runner із Docker, systemd, firewall і базовою стратегією резервного копіювання. Він може виконувати CI/CD workflow, збирати контейнери, тестувати код і деплоїти застосунки без потреби відкривати вхідні порти для самого агента.

  1. Створіть окремі labels і runner groups для CI, staging і production, щоб production-job не виконувалися на звичайній машині розробки.
  2. Додайте моніторинг диска, пам’яті, статусу systemd та успішності бекапів; CI найчастіше ламається через заповнене Docker-сховище.
  3. Зі зростанням навантаження розподіліть завдання між кількома runner або перейдіть на ephemeral runners для недовірених і публічних workflow.

Чи був цей гайд корисним?

Ваш відгук допомагає нам покращувати гайди.

Share this post:

Надішліть гайд тому, кому він може стати в пригоді.

Telegram VKVK WhatsApp Facebook LinkedIn XX

ci/cd на власному vps: github actions self-hosted runner
support_agent
Valebyte Support
Usually replies within minutes
Hi there!
Send us a message and we'll reply as soon as possible.