惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threat Research - Cisco Blogs
A
Arctic Wolf
S
Securelist
O
OpenAI News
T
Threatpost
Forbes - Security
Forbes - Security
N
News and Events Feed by Topic
S
Secure Thoughts
H
Heimdal Security Blog
S
Security Affairs
P
Privacy International News Feed
C
Cisco Blogs
C
CERT Recently Published Vulnerability Notes
Cyberwarzone
Cyberwarzone
N
News and Events Feed by Topic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Palo Alto Networks Blog
S
Security @ Cisco Blogs
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 三生石上(FineUI控件)
月光博客
月光博客
T
Tailwind CSS Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hacker News: Ask HN
Hacker News: Ask HN
T
Troy Hunt's Blog
S
SegmentFault 最新的问题
腾讯CDC
V
Visual Studio Blog
Last Week in AI
Last Week in AI
H
Hacker News: Front Page
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Project Zero
Project Zero
WordPress大学
WordPress大学
NISL@THU
NISL@THU
博客园 - 【当耐特】
博客园 - Franky
Webroot Blog
Webroot Blog
博客园_首页
T
Tenable Blog
雷峰网
雷峰网
Google Online Security Blog
Google Online Security Blog
阮一峰的网络日志
阮一峰的网络日志
V2EX - 技术
V2EX - 技术
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
Lohrmann on Cybersecurity
The Hacker News
The Hacker News

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Личный CI/CD за один вечер: настраиваем GitLab Runner на собственном VPS
Анна Гамгия · 2026-06-03 · via Все публикации подряд на Хабре

Простой

13 мин

11K

Если у вас пет-проект или небольшой стартап на 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.