MAATRIX / Блог / "GitLab CI/CD на VPS: настройка"

"GitLab CI/CD на VPS: настройка"

"GitLab CI/CD на VPS: настройка"

MAATRIX

Если пайплайн в GitLab подолгу стоит в очереди «This job is stuck», а лимит бесплатных CI-минут на gitlab.com заканчивается к середине месяца — проблема решается за час: собственный GitLab Runner на своём VPS. Он подключается к любому проекту (и на gitlab.com, и на self-hosted GitLab), не считает минуты и не делит очередь с чужими задачами. Ниже — пошаговая установка, выбор executor и рабочий пример .gitlab-ci.yml с этапами build/test/deploy.

Зачем свой раннер, если есть shared-раннеры gitlab.com

Shared-раннеры gitlab.com удобны для старта, но у них есть практические ограничения, которые чувствуются на активном проекте:

  • Лимит CI/CD-минут. На бесплатном тарифе это несколько сотен минут в месяц на всю группу — общий пайплайн с тестами и сборкой Docker-образа съедает их быстро.
  • Очередь. В пиковые часы задания могут ждать свободный shared-раннер минуты, иногда дольше — вы не контролируете, сколько людей стоит перед вами.
  • Окружение «как дадут». Версии инструментов, объём диска, доступная память — стандартные, под конкретный проект их не подогнать.
  • Нет доступа к внутренней сети. Если деплой идёт на закрытый сервер или БД без публичного доступа, shared-раннер до неё физически не достучится.

Self-hosted раннер на своём VPS снимает все четыре пункта разом: минуты не считаются (тарификация только по ресурсам самого VPS), очереди нет — раннер занят только вашими задачами, окружение настраиваете сами (нужные версии Node/PHP/Python, объём диска под кэш и артефакты), а если раннер и продакшен-сервер в одной приватной сети — деплой становится тривиальным ssh без пробрасывания портов наружу.

Для проекта с редкими коммитами разница не критична. Но если пайплайн гоняется по 10–20 раз в день (feature-ветки, MR-пайплайны, ночные сборки) — окупаемость своего раннера на VPS считается за первую же неделю.

Установка GitLab Runner на VPS

Официальный способ — репозиторий GitLab через скрипт-инсталлятор. Дальше пример для Ubuntu/Debian (для RHEL-семейства команда apt меняется на yum/dnf, сам скрипт это определяет автоматически).

# скачиваем и подключаем официальный репозиторий GitLab Runner
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash

# ставим пакет
sudo apt install gitlab-runner

Скрипт сам добавляет GPG-ключ и репозиторий apt, поэтому обновления раннера потом идут обычным apt update && apt upgrade. После установки сервис уже поднят через systemd:

sudo systemctl status gitlab-runner
sudo systemctl enable gitlab-runner   # автозапуск при перезагрузке VPS

Проверить версию:

gitlab-runner --version

На момент написания актуальная линейка — 17.x, но конкретную версию, которую вам поставит скрипт, лучше сверить на самой странице пакета — GitLab выпускает релизы часто, и жёстко фиксировать номер версии в статье смысла нет.

Если раннер будет собирать Docker-образы или запускать джобы в контейнерах (executor docker), Docker нужно поставить отдельно — это не входит в пакет gitlab-runner. Подробный разбор установки Docker с нуля — в статье про Docker на Ubuntu 24.04.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Регистрация раннера в проекте

Раннер регистрируется токеном проекта (или группы — если хотите один раннер на несколько репозиториев). Токен смотрите в проекте: Settings → CI/CD → Runners → New project runner. Там же задаются теги и флаг «run untagged jobs».

С новыми версиями GitLab Runner (16.x и выше) регистрация выглядит так:

sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --token "glrt-ВАШ_ТОКЕН_РАННЕРА" \
  --executor "docker" \
  --docker-image "alpine:latest" \
  --description "vps-runner-01" \
  --tag-list "vps,docker,prod"

Ключевые параметры:

  • --url — адрес вашего GitLab (для self-hosted GitLab это адрес вашего сервера, не gitlab.com);
  • --token — токен из настроек проекта/группы (в новых версиях начинается с glrt-, в старых использовался общий registration-token — если у вас старая версия GitLab, синтаксис регистрации отличается, сверьтесь с документацией конкретной версии);
  • --executor — как раннер будет выполнять джобы (разбор ниже);
  • --tag-list — теги, по которым .gitlab-ci.yml выбирает именно этот раннер через tags:.

После регистрации конфигурация лежит в /etc/gitlab-runner/config.toml — можно смотреть и править руками (лимит параллельных джобов, таймауты, кэш) без повторной регистрации, только перезапустите сервис:

sudo systemctl restart gitlab-runner

Проверить, что раннер появился и online — в том же разделе Settings → CI/CD → Runners проекта: должен появиться зелёный индикатор.

Executor: shell или docker

Executor определяет, где именно выполняется код джобы. Дляself-hosted раннера на VPS чаще всего выбирают между двумя вариантами.

shelldocker
Изоляция джобнет — все джобы работают в одной ОСда — каждая джоба в своём контейнере
Скорость стартабыстрее (нет накладных расходов на контейнер)медленнее на старте (pull образа, если его нет в кэше)
Чистота окружениязависимости и мусор от прошлых джоб накапливаютсяокружение всегда «с нуля» из образа
Настройкаминимальная — раннер использует инструменты, установленные на VPSнужен установленный Docker, образы под каждый стек
Риск для хостаджоба выполняется от имени пользователя gitlab-runner с доступом к файловой системе VPSджоба ограничена контейнером, доступ к хосту — только через явные volume/socket
Подходит дляпростых пайплайнов, одного проекта, где вы доверяете коду в репозиториинескольких проектов на раннере, разных стеков (Node/PHP/Python на одном VPS), CI для внешних контрибьюторов

shell executor проще всего: раннер выполняет команды джобы напрямую в системе, от имени системного пользователя gitlab-runner. Подходит, если раннер обслуживает один-два ваших проекта и вы контролируете весь код, который туда попадает. Минус — зависимости разных проектов (версии Node, Python, глобальные npm-пакеты) начинают конфликтовать, если раннер общий.

docker executor запускает каждую джобу в отдельном контейнере на основе указанного образа (--docker-image, либо через image: прямо в .gitlab-ci.yml). Это изолирует джобы друг от друга и от хоста — сборка на Node 20 не испортит окружение для джобы на PHP 8.3. Требует установленного Docker на VPS и делает раннер более требовательным к диску: каждый образ и слои кэшируются локально, за местом стоит следить (см. статью про переполнение диска Docker).

Практическая рекомендация: если раннер один и обслуживает единственный проект с доверенным кодом — берите shell, это проще в администрировании. Если раннер общий на несколько проектов или пайплайны собирают Docker-образы — берите docker executor, изоляция того стоит.

Пример .gitlab-ci.yml с этапами build/test/deploy

Базовый пайплайн для Node.js-проекта с деплоем по SSH на продакшен-сервер (тот же VPS или соседний):

stages:
  - build
  - test
  - deploy

variables:
  NODE_ENV: "production"

build_job:
  stage: build
  image: node:20-alpine
  tags:
    - docker
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

test_job:
  stage: test
  image: node:20-alpine
  tags:
    - docker
  script:
    - npm ci
    - npm test

deploy_job:
  stage: deploy
  image: alpine:latest
  tags:
    - docker
  only:
    - main
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
  script:
    - scp -r dist/* deploy@$DEPLOY_HOST:/var/www/app/
    - ssh deploy@$DEPLOY_HOST "sudo systemctl restart app"

Пояснения:

  • tags: [docker] — джоба уйдёт именно на раннер с этим тегом (полезно, когда раннеров несколько: один для сборки, другой для деплоя во внутреннюю сеть);
  • artifacts — файлы из build_job передаются в следующие стадии без пересборки;
  • only: [main] — деплой запускается только из ветки main, фиче-ветки прогоняют build и test, но не трогают продакшен;
  • SSH_PRIVATE_KEY и DEPLOY_HOST — задаются в Settings → CI/CD → Variables проекта, отмечены как Protected и Masked, чтобы не светиться в логах и не работать на незащищённых ветках.

Для деплоя через Docker Compose вместо scp/ssh схема аналогична: собираете образ на стадии build, пушите в приватный registry (см. настройку приватного Docker registry на VPS), на стадии deploy по SSH делаете docker compose pull && docker compose up -d.

Безопасность и изоляция раннера

Раннер, который умеет по SSH заходить на продакшен, — привлекательная цель, если в него попадёт чужой код (например, через merge request из форка). Несколько практических мер:

  • Не давайте раннеру privileged: true без необходимости. Этот режим docker executor нужен только для Docker-in-Docker (сборка образов внутри джобы) — если просто разворачиваете готовый образ, он не нужен.
  • Ограничьте concurrent в /etc/gitlab-runner/config.toml. Значение по умолчанию может позволить раннеру брать несколько тяжёлых джоб одновременно и упереться в память VPS — под средний проект достаточно 2–4.
  • Не запускайте раннер от root. Установщик по умолчанию создаёт системного пользователя gitlab-runner с ограниченными правами — не переопределяйте это без веской причины.
  • Protected переменные и Protected-ветки. SSH-ключи и токены деплоя держите как Protected-переменные, а деплой запускайте только с защищённых веток (main/production) — тогда MR из форков их не увидят.
  • Отдельный раннер для внешних контрибьюторов. Если проект открытый и принимает MR извне, разумно завести второй раннер без доступа к продакшен-сети специально под их пайплайны.
  • Firewall на VPS. Раннеру не нужны входящие соединения — исходящие к gitlab.com/self-hosted GitLab и к серверу деплоя достаточно. Базовая настройка ufw описана в статье про firewall UFW на VPS.

Мониторинг самого раннера (память, диск, доступность сервиса) стоит подключить тем же способом, что и для остальных сервисов на VPS — например, через Uptime Kuma или связку Grafana и Prometheus.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли подключить раннер на VPS к self-hosted GitLab, а не только к gitlab.com?

Да, при регистрации просто укажите --url вашего self-hosted GitLab вместо https://gitlab.com/ — остальная настройка идентична.

Сколько раннеров можно зарегистрировать на один VPS?

Формально сколько угодно — каждый со своим тегом и (опционально) executor'ом. Практический предел — ресурсы VPS: несколько параллельных docker-джоб быстро съедают RAM и CPU, ориентируйтесь на 2 vCPU / 4 ГБ RAM как стартовую конфигурацию для одного активного раннера.

Что делать, если раннер показывает статус offline?

Проверьте systemctl status gitlab-runner и исходящий доступ к GitLab (curl -I https://gitlab.com) — чаще всего причина в firewall, блокирующем исходящие соединения, или в истёкшем токене после ручной смены URL проекта.

Нужен ли Docker, если executor — shell?

Нет, shell executor выполняет команды джобы напрямую в системе раннера, Docker не требуется вовсе — но тогда все нужные для сборки инструменты (Node, компиляторы, CLI) нужно ставить на VPS вручную.

Как обновить GitLab Runner на VPS?

Через тот же пакетный менеджер, которым он был установлен: sudo apt update && sudo apt install gitlab-runner. Конфигурация в config.toml при обновлении сохраняется.