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

Получить VPS arrow_forward
eco Начальный Туториал

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

calendar_month Sep 16, 2026 schedule 24 мин. чтения visibility 20 просмотров
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 и постоянный кэш

Для маленького private-репозитория 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 или отдельные одноразовые виртуальные машины.

В этом руководстве подразумевается private-репозиторий либо команда с контролируемым доступом на запись. Для 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 под private-репозиторий разумная стартовая конфигурация — 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.

Troubleshooting + 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.

Был ли этот гайд полезен?

Ваш отзыв помогает нам улучшать гайды.

Поделиться записью:

Отправьте гайд тому, кому он может пригодиться.

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.