
Если у вас пет-проект или небольшой стартап на GitLab.com, рано или поздно вы упрётесь в потолок бесплатного тарифа: 400 минут пайплайнов в месяц и общая очередь раннеров. Покупка дополнительных минут стоит денег и не решает вторую проблему: общие раннеры GitLab обслуживают миллионы проектов, и в часы пик ваша джоба может провисеть в очереди 10-20 минут.
Решение — свой GitLab Runner на VPS: без чужих джоб, под полным контролем. Такой раннер не имеет лимитов по времени, кроме ресурсов самого сервера. В статье за вечер собираем такой раннер с нуля на Ubuntu 24.04 LTS, поднимаем пайплайн на три стадии (тесты, сборка Docker-образов, пуш в GitLab Container Registry), добавляем кэширование, безопасность и автообновление.
Что такое GitLab Runner и зачем он нужен
GitLab Runner — это агент, который выполняет джобы пайплайна. Сам GitLab код из .gitlab-ci.yml не выполняет — он работает как диспетчер: получает push, парсит конфигурацию, разбивает её на джобы и ждёт, пока раннер заберёт задание.
Раннер работает по модели pull. Он постоянно опрашивает GitLab-сервер через API: отправляет POST-запрос на /api/v4/jobs/request, передаёт свой токен аутентификации и список тегов. Если в очереди есть джоба, подходящая под теги этого раннера, GitLab возвращает её описание в формате JSON.
В описании содержатся:
Docker-образ, который нужно использовать;
команды из секции script;
переменные окружения;
настройки артефактов и кэша.
Период опроса настраивается параметром check_interval в файле /etc/gitlab-runner/config.toml и по умолчанию составляет 3 секунды. Это компромисс между оперативностью и нагрузкой на GitLab API.
Типы раннеров отличаются тем, кто их может использовать и в каких проектах они доступны. Shared runners (общие раннеры) предоставляются GitLab. Они доступны всем проектам на платформе, и на них распространяются лимиты минут. Group runners привязаны к группе проектов и доступны всем проектам внутри этой группы. Project runners закреплены за конкретным проектом и работают только для него. Для личного проекта достаточно project runner — так проще контролировать доступ.
Теперь разберёмся с экзекуторами
Экзекутор определяет, в каком окружении раннер будет выполнять джобы. Shell-экзекутор выполняет команды прямо на хосте через bash. Он самый простой в настройке, но не обеспечивает изоляции: джоба имеет доступ ко всем файлам сервера, может устанавливать пакеты в систему и потенциально способна что-то сломать. Параллельные джобы на shell-экзекуторе могут конфликтовать за ресурсы и файлы.
Docker-экзекутор для каждой джобы создаёт отдельный контейнер на основе заданного образа. После завершения джобы контейнер автоматически удаляется, и окружение остаётся чистым. Это дает изоляцию и воспроизводимость: джоба всегда запускается в одинаковом окружении, независимо от того, что происходило на сервере до этого. Docker-экзекутор также позволяет использовать любые образы из Docker Hub или приватных registry, что делает его подходящим для большинства задач.
Kubernetes-экзекутор предназначен для проектов, где раннеры работают внутри k8s-кластера. Каждая джоба запускается в отдельном поде, который удаляется после завершения. Мы будем использовать Docker-экзекутор как удачный баланс между изоляцией, простотой и возможностями.
Что потребуется для повторения
Для настройки раннера понадобится VPS минимальной конфигурации: 1 ядро, 2 ГБ оперативной памяти и 20 ГБ дискового пространства. Этого достаточно для типовых пайплайнов на Python, Node.js или Go. Если планируете собирать тяжёлые Docker-образы, можно взять 4 ГБ памяти, но начинать лучше с минимальной конфигурации и масштабироваться по мере необходимости.
Используем Ubuntu 24.04 LTS. Все команды проверены на этой версии. Главное требование к ОС: доступ к официальным репозиториям Docker и GitLab Runner, которые есть для всех поддерживаемых версий Ubuntu.
Ещё понадобится аккаунт на GitLab.com и проект, на котором будем тестировать настройку. Мы создадим простой проект и пройдём все шаги от начала до конца.
Шаг 1: Создаём проект и отключаем общие раннеры
Прежде чем настраивать сервер, создадим проект на GitLab.com (при регистрации нового аккаунта это можно сделать сразу через регистрационный визард или позже вручную) и сразу решим проблему, с которой сталкиваются пользователи из России.
Создаём проект
Зайдите на gitlab.com и войдите в аккаунт (или зарегистрируйтесь, это бесплатно).
Нажмите кнопку New project в правом верхнем углу.
Выберите Create blank project.
В поле Project name введите
my-ci-project.Поставьте галку Include a Getting Started README.
Нажмите Create project.
Важное примечание для пользователей из России
На май 2026 года GitLab.com может запросить верификацию аккаунта по номеру телефона для запуска CI/CD пайплайнов. Российские номера (+7) в списке поддерживаемых стран отсутствуют, поэтому пройти такую проверку невозможно. Запрос верификации чаще всего появляется при использовании общих раннеров (shared runners). Если у вас есть собственный раннер, верификация не нужна, потому что GitLab считает, что вы используете своё оборудование, а не его инфраструктуру.
Решение: отключаем общие раннеры на уровне группы. Это нужно сделать до первого запуска пайплайна, иначе вы увидите окно верификации и не сможете продолжить.
Перейдите в настройки своей группы: откройте группу, в которой создали проект, и перейдите в Settings → CI/CD. Или воспользуйтесь прямой ссылкой:
https://gitlab.com/groups/ВАША_ГРУППА/-/settings/ci\_cd.Найдите переключатель Enable instance runners for this group.
Отключите его.
После этого пайплайны будут запускаться только на вашем собственном раннере.
Шаг 2: Создаём VPS и устанавливаем Docker
Подключаемся к серверу по SSH. Первым делом установим Docker. Docker нужен не только как экзекутор. Позже при сборке Docker-образов в пайплайне мы применим подход Docker-out-of-Docker — для него нужен Docker на хосте.
Docker устанавливаем из официального репозитория, а не из репозиториев Ubuntu. Пакет docker.io в репозиториях Ubuntu часто отстаёт на несколько версий, а нам нужен актуальный Docker с поддержкой Buildx и современных драйверов кэширования. Добавляем GPG-ключ и репозиторий:
sudo apt update sudo apt install -y ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Разберём, зачем нужен каждый пакет:
docker-ce— служба Docker, которая управляет контейнерами, образами, сетями и томами.docker-ce-cli— командный интерфейс, с помощью которого мы отдаём команды службе.containerd.io— низкоуровневый рантайм, управляющий жизненным циклом контейнера.docker-buildx-plugin— плагин для продвинутой сборки образов с поддержкой кэширования.docker-compose-plugin— плагин для работы с docker compose.
После установки проверяем, что Docker запущен и отвечает на команды:
sudo docker run hello-world
Если появилось сообщение «Hello from Docker!», всё работает.

Теперь добавляем пользователя в группу docker, чтобы не вводить sudo перед каждой командой. Это особенно важно для GitLab Runner, который будет обращаться к Docker от имени пользователя gitlab-runner:
sudo usermod -aG docker $USER
После этого выйдите из SSH-сессии и зайдите заново, чтобы изменения применились. Проверьте, что docker ps работает без sudo.
Шаг 3: Устанавливаем и регистрируем GitLab Runner
Установка раннера начинается с добавления официального репозитория GitLab:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash sudo apt install -y gitlab-runner
Теперь главное — регистрация раннера в проекте. На май 2026 года старый метод с регистрационным токеном (registration token) больше не работает. Теперь нужен аутентификационный токен — его создают через кнопку New project runner.
Заходим в проект на GitLab.com, открываем Settings → CI/CD → Runners.
Нажимаем кнопку Create project runner.
-
В открывшейся форме указываем:
Теги:
my-runnerСтавим галку Run untagged jobs (чтобы раннер брал джобы без тегов).
Нажимаем Create runner.
Выбираем Linux.
Нажимаем View runners. GitLab генерирует аутентификационный токен и показывает готовую команду для регистрации. Копируем её и выполняем на VPS:
sudo gitlab-runner register --url https://gitlab.com --token glrt-xxxxxxxxxxxxxx
В интерактивном режиме потребуется ответить на несколько вопросов:
«Enter the GitLab instance URL» — оставляем
https://gitlab.com, нажимаем Enter.«Enter a name for the runner» — нажимаем Enter (имя по умолчанию).
«Enter an executor» — вводим
dockerи нажимаем Enter.«Enter the default Docker image» — вводим
ubuntu:24.04, нажимаем Enter.
Примечание: В некоторых версиях раннера может возникнуть ошибка «Invalid executor specified» при вводе docker. Если это произошло, прервите регистрацию (Ctrl+C) и используйте неинтерактивный режим:
sudo gitlab-runner register --non-interactive --url https://gitlab.com --token glrt-... --executor docker
После регистрации добавьте Docker-конфигурацию /etc/gitlab-runner/config.toml. Откройте его:
sudo nano /etc/gitlab-runner/config.toml
Найдите секцию [[runners]] и убедитесь, что там есть блок [runners.docker]. Если блока нет, добавьте его под строкой executor = "docker":
[runners.docker] image = "ubuntu:24.04" volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
Сохраните файл (Ctrl+O, Enter) и выйдите (Ctrl+X). Перезапустите раннер:
sudo gitlab-runner restart
После завершения регистрации раннер появляется в Settings → CI/CD → Runners с зелёным кружком. Это означает, что раннер подключён к GitLab и готов принимать задания.

Шаг 4: Пишем минимальный пайплайн и проверяем работу
Создадим в корне проекта файл .gitlab-ci.yml. Перейдите в Code → Repository, нажмите + → New file. В поле File name введите .gitlab-ci.yml. В большое текстовое поле вставьте код:
stages: - test hello_world: stage: test image: ubuntu:24.04 script: - echo "Привет, CI/CD!" - uname -a tags: - my-runner
Разберём каждую строку. Ключ stages определяет список стадий и их порядок. Стадии выполняются последовательно: сначала все джобы test, потом build, потом deploy. Если джоба на любой из стадий падает, следующие стадии по умолчанию не выполняются.
Имя джобы hello_world может быть любым — оно отображается в интерфейсе GitLab. Параметр stage указывает, к какой стадии принадлежит джоба. Параметр image задаёт Docker-образ, в котором джоба будет выполняться. Параметр script — список команд; они выполняются в контейнере последовательно. Если какая-то завершается с ненулевым кодом, джоба падает.
Сразу добавляем tags: my-runner. Общие раннеры отключены на уровне группы, поэтому пайплайн пойдёт только на нашем раннере. Тег my-runner указывает GitLab, на каком именно раннере запускать джобу.
Нажмите Commit changes. После коммита перейдите в Build → Pipelines. Вы увидите первый пайплайн. Нажмите на джобу hello_world, чтобы посмотреть логи выполнения. Если всё настроено правильно, вы увидите вывод команд echo и uname -a.


Шаг 5: Базовая безопасность раннера
Настроим защиту раннера. В Settings → CI/CD → Runners нажмите на карандаш рядом с вашим раннером. Поставьте галку Protected (Защищённый). Это означает, что раннер будет принимать джобы только из защищённых веток (например, main). Нажмите Save changes.
Также в Settings → CI/CD найдите настройку Allow merge request pipelines to access protected variables and runners и поставьте эту галку. Она разрешает MR-пайплайнам доступ к защищённым переменным и раннерам, когда и исходная, и целевая ветка защищены.
Защита переменных
Если вы храните токены или пароли в переменных CI/CD, их нужно защитить. Перейдите в Settings → CI/CD → Variables. Нажмите Add variable. Создайте тестовую переменную:
Key:
TEST_TOKENValue:
test12345Поставьте галку Protected (переменная доступна только в защищённых ветках).
Поставьте галку Masked (значение будет скрыто в логах).
Нажмите Add variable.
Зачем это нужно: для личного пет-проекта эти настройки не критичны — ваш сервер и так никому не доступен. Но мы ставим флаги сейчас, чтобы сформировать правильную привычку. Когда проект вырастет и в нём появятся другие разработчики, эти настройки уже будут готовы.


Шаг 6: Усложняем пайплайн до 3 стадий со сборкой Docker-образов
Расширим пайплайн до полноценного CI/CD из трёх стадий: test (запуск тестов), build (сборка Docker-образа приложения) и deploy (пуш образа в GitLab Container Registry).
Для сборки Docker-образов используем подход Docker-out-of-Docker (DooD). Вместо запуска отдельного демона Docker внутри контейнера мы монтируем сокет /var/run/docker.sock с хоста в контейнер раннера. Когда джоба выполняет команду docker build, Docker-клиент внутри контейнера обращается к службе на хосте через этот сокет. Все слои образов сохраняются в хранилище хоста и доступны между джобами. Мы уже настроили это в config.toml на шаге 3.
Прежде чем писать пайплайн, создадим три файла, без которых он упадёт.
Перейдите в Code → Repository, нажмите + → New file.
Создайте файл Dockerfile:
FROM ubuntu:24.04 CMD echo "Hello from Docker!"
Создайте файл requirements.txt:
pytest
Создайте файл test_example.py:
def test_dummy(): assert True
Теперь отредактируйте .gitlab-ci.yml (Code → Repository, нажмите на файл, затем Edit) и замените всё содержимое:
stages: - test - build - deploy variables: DOCKER_HOST: unix:///var/run/docker.sock test: stage: test image: python:3.13-slim script: - pip install -r requirements.txt - pytest tags: - my-runner build: stage: build image: docker:latest script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA . tags: - my-runner deploy: stage: deploy image: docker:latest script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA rules: - if: $CI_COMMIT_BRANCH == "main" tags: - my-runner
Что означают переменные:
DOCKER_HOST: unix:///var/run/docker.sock— указывает Docker-клиенту внутри контейнера обращаться к службе на хосте через сокет, а не пытаться найти Docker внутри контейнера.$CI_REGISTRY_IMAGE— полный путь к образу в GitLab Container Registry (имеет видregistry.gitlab.com/username/project).$CI_REGISTRY_USERи$CI_REGISTRY_PASSWORD— временные учётные данные, которые GitLab генерирует автоматически для каждой джобы.$CI_COMMIT_SHORT_SHA— первые 8 символов хеша коммита, которые используются как тег образа.
Нажмите Commit changes. Перейдите в Build → Pipelines и дождитесь завершения. Все три этапа должны стать зелёными.

Шаг 7: Настройка кэширования зависимостей
При каждом запуске джобы test контейнер создаётся с нуля, и pip заново скачивает все зависимости. Даже если список зависимостей не изменился, установка занимает десятки секунд, а на крупных проектах — минуты. Кэширование решает эту проблему: зависимости скачиваются один раз, сохраняются и переиспользуются при следующих запусках. Самый надёжный способ для Python — кэшировать не кэш pip, а всё виртуальное окружение (venv).
Отредактируйте секцию test в .gitlab-ci.yml:
test: stage: test image: python:3.13-slim variables: VENV_DIR: "./venv" cache: key: files: - requirements.txt paths: - venv/ before_script: - if [ ! -d "$VENV_DIR" ]; then python -m venv $VENV_DIR; fi - source $VENV_DIR/bin/activate script: - pip install --upgrade pip - pip install -r requirements.txt - pytest tags: - my-runner
Что здесь происходит:
Переменная
VENV_DIRзадаёт относительный путь к папке виртуального окружения внутри проекта.Секция
cacheсохраняет папкуvenv/после успешного выполнения джобы (ключ кэша привязан к содержимомуrequirements.txt).before_scriptпроверяет, существует ли уже папка с окружением, и создаёт её только при необходимости, после чего активирует виртуальное окружение.scriptобновляет pip и устанавливает зависимости — все они попадают в папкуvenv, которая будет закэширована и восстановлена при следующих запусках.
Запустите пайплайн два раза подряд. При первом запуске pip скачает и установит пакеты в venv. При втором запуске GitLab восстановит папку venv из кэша, и pip проверит, что все пакеты уже есть (вы увидите Requirement already satisfied). Установка займёт секунды.

Шаг 8: Продвинутое Docker-кэширование через registry
Установку зависимостей мы ускорили, но осталась вторая проблема — сборка Docker-образов. Образ собирается послойно. Если в контексте сборки изменился хотя бы один файл, Docker пересобирает все слои, начиная с изменившегося. Docker Buildx умеет сохранять кэш слоёв в удалённом registry и загружать его при следующей сборке. Мы используем GitLab Container Registry, который встроен в каждый проект.
Замените .gitlab-ci.yml полностью:
stages: - test - build-push - deploy variables: DOCKER_HOST: unix:///var/run/docker.sock test: stage: test image: python:3.13-slim variables: VENV_DIR: "./venv" cache: key: files: - requirements.txt paths: - venv/ before_script: - if [ ! -d "$VENV_DIR" ]; then python -m venv $VENV_DIR; fi - source $VENV_DIR/bin/activate script: - pip install --upgrade pip - pip install -r requirements.txt - pytest tags: - my-runner build-push: stage: build-push image: docker:latest before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker buildx create --use --name my-builder --driver docker-container --bootstrap script: - docker buildx build --cache-from=type=registry,ref=$CI_REGISTRY_IMAGE/cache --cache-to=type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA --push . rules: - if: $CI_COMMIT_BRANCH == "main" tags: - my-runner deploy: stage: deploy image: docker:latest script: - echo "Image pushed $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" rules: - if: $CI_COMMIT_BRANCH == "main" tags: - my-runner
Как это работает:
Стадия
build-pushлогинится в GitLab Container Registry (переменные$CI_REGISTRY_USERи$CI_REGISTRY_PASSWORDпредоставляются GitLab автоматически).Создаётся билдер
my-builderс драйверомdocker-container— он необходим для экспорта кэша в registry (стандартный драйверdockerэтого не умеет).Команда
docker buildx buildсобирает образ. Флаг--cache-fromзагружает предыдущий кэш из registry (если он есть), флаг--cache-toсохраняет новый кэш после сборки. Флаг--pushпушит готовый образ в registry.Отдельная стадия для предварительной загрузки кэша не требуется:
docker buildxсам загружает кэш при наличии логина и настроенного билдера.Флаг
mode=maxкэширует все промежуточные слои, что ускоряет сборку даже при изменениях в серединеDockerfile.

Что дальше
Мы прошли путь от пустого VPS до полноценного CI/CD с собственным раннером. Зафиксируем, что настроено и почему это работает.
Раннер использует Docker-out-of-Docker: сокет Docker с хоста монтируется в контейнер, поэтому джобы могут собирать образы без привилегированного режима, а кэш слоёв сохраняется на хосте.
Мы отключили общие раннеры GitLab на уровне группы, чтобы пайплайны выполнялись только на нашем сервере и не требовали верификации по телефону.
Раннер привязан к защищённым веткам, чтобы джобы из MR не получили доступа к файловой системе сервера.
Настроено кэширование зависимостей Python через виртуальное окружение (venv): папка с окружением сохраняется и переиспользуется между джобами, что исключает повторную установку пакетов.
Настроено кэширование Docker-слоёв через GitLab Container Registry с использованием Buildx: кэш загружается перед сборкой и сохраняется после неё.
При росте проекта раннер масштабируется в двух направлениях:
Вертикальное масштабирование: увеличение ресурсов VPS и параметра
concurrentв/etc/gitlab-runner/config.tomlпозволяет выполнять несколько джоб параллельно на одном сервере.Горизонтальное масштабирование: добавление второго VPS со своим раннером, зарегистрированным с теми же тегами, — GitLab автоматически распределяет джобы между ними.
Последний штрих — автообновление. GitLab Runner обновляется часто, и если пропустить несколько версий, раннер может перестать принимать джобы с ошибкой «версия раннера устарела». Чтобы раннер обновлялся автоматически, добавьте задачу в системный cron через отдельный файл в каталоге /etc/cron.d/. Это безопаснее, чем редактировать пользовательский crontab, так как не затирает другие задания. Выполните на сервере:
echo "0 3 * * 1 root apt update && apt install -y gitlab-runner -qq" | sudo tee /etc/cron.d/gitlab-runner-update
Эта команда создаст файл /etc/cron.d/gitlab-runner-update с расписанием: каждую неделю в понедельник в 3 часа ночи от имени root будет выполняться обновление пакета gitlab-runner.
Полезные ссылки для дальнейшего изучения
Официальная документация GitLab Runner: https://docs.gitlab.com/runner/
Все параметры config.toml: https://docs.gitlab.com/runner/configuration/advanced-configuration.html
Документация Docker Buildx (Build drivers): https://docs.docker.com/build/builders/drivers/
Кэширование в Docker Buildx (Cache storage backends): https://docs.docker.com/build/cache/backends/
GitLab Container Registry: https://docs.gitlab.com/ee/user/packages/container_registry/
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.