"GitLab CI/CD на VPS: настройка"
Если пайплайн в 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 чаще всего выбирают между двумя вариантами.
| shell | docker | |
|---|---|---|
| Изоляция джоб | нет — все джобы работают в одной ОС | да — каждая джоба в своём контейнере |
| Скорость старта | быстрее (нет накладных расходов на контейнер) | медленнее на старте (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 при обновлении сохраняется.