Если у вас пет-проект или небольшой стартап на 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 считает, что вы используете своё оборудование, а не его инфраструктуру.

Решение: отключаем общие раннеры на уровне группы. Это нужно сделать до первого запуска пайплайна, иначе вы увидите окно верификации и не сможете продолжить.

  1. Перейдите в настройки своей группы: откройте группу, в которой создали проект, и перейдите в Settings → CI/CD. Или воспользуйтесь прямой ссылкой: https://gitlab.com/groups/ВАША_ГРУППА/-/settings/ci\_cd.

  2. Найдите переключатель Enable instance runners for this group.

  3. Отключите его.

После этого пайплайны будут запускаться только на вашем собственном раннере.

Шаг 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!», всё работает.

Терминал с выводом «Hello from Docker!»
Терминал с выводом «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.

  1. Заходим в проект на GitLab.com, открываем Settings → CI/CD → Runners.

  2. Нажимаем кнопку Create project runner.

  3. В открывшейся форме указываем:

    • Теги: my-runner

    • Ставим галку Run untagged jobs (чтобы раннер брал джобы без тегов).

  4. Нажимаем Create runner.

  5. Выбираем Linux.

  6. Нажимаем 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 и готов принимать задания.

Страница Settings → CI/CD → Runners с зелёным кружком Online
Страница Settings → CI/CD → Runners с зелёным кружком Online

Шаг 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.

Страница Pipelines с зелёным статусом Passed
Страница Pipelines с зелёным статусом Passed
Логи джобы hello_world с выводом «Привет, CI/CD!» и uname -a
Логи джобы hello_world с выводом «Привет, CI/CD!» и 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_TOKEN

  • Value: test12345

  • Поставьте галку Protected (переменная доступна только в защищённых ветках).

  • Поставьте галку Masked (значение будет скрыто в логах).

  • Нажмите Add variable.

Зачем это нужно: для личного пет-проекта эти настройки не критичны — ваш сервер и так никому не доступен. Но мы ставим флаги сейчас, чтобы сформировать правильную привычку. Когда проект вырастет и в нём появятся другие разработчики, эти настройки уже будут готовы.

Страница Variables с переменной TEST_TOKEN и флагами Protected, Masked
Страница Variables с переменной TEST_TOKEN и флагами Protected, Masked
Страница редактирования раннера с установленной галкой Protected
Страница редактирования раннера с установленной галкой Protected

Шаг 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 и дождитесь завершения. Все три этапа должны стать зелёными.

Страница пайплайна с тремя успешными этапами: build, test, deploy
Страница пайплайна с тремя успешными этапами: build, test, deploy

Шаг 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). Установка займёт секунды.

Лог джобы test при повторном запуске. Видно, что зависимости уже установлены в виртуальном окружении (Requirement already satisfied)
Лог джобы test при повторном запуске. Видно, что зависимости уже установлены в виртуальном окружении (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.

Сохранение кэша Docker-слоёв в registry
Сохранение кэша Docker-слоёв в registry

Что дальше

Мы прошли путь от пустого 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.

Полезные ссылки для дальнейшего изучения


НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.

Комментарии (0)