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

推荐订阅源

Jina AI
Jina AI
V
Vulnerabilities – Threatpost
Security Latest
Security Latest
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
量子位
H
Help Net Security
Attack and Defense Labs
Attack and Defense Labs
The GitHub Blog
The GitHub Blog
L
LINUX DO - 最新话题
A
Arctic Wolf
博客园_首页
S
Securelist
S
Secure Thoughts
Google DeepMind News
Google DeepMind News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
N
Netflix TechBlog - Medium
Cyberwarzone
Cyberwarzone
小众软件
小众软件
T
Threatpost
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Blog — PlanetScale
Blog — PlanetScale
N
News and Events Feed by Topic
NISL@THU
NISL@THU
Forbes - Security
Forbes - Security
博客园 - 聂微东
F
Fortinet All Blogs
Simon Willison's Weblog
Simon Willison's Weblog
H
Heimdal Security Blog
罗磊的独立博客
S
Security @ Cisco Blogs
B
Blog
T
Troy Hunt's Blog
Engineering at Meta
Engineering at Meta
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
Hacker News - Newest:
Hacker News - Newest: "LLM"
I
Intezer
T
Threat Research - Cisco Blogs
C
Cybersecurity and Infrastructure Security Agency CISA
The Cloudflare Blog
S
Schneier on Security
月光博客
月光博客
L
LINUX DO - 热门话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 Registry, Docker in Docker и отказоустойчивость
Гончаров Тимофей · 2026-06-07 · via Все публикации подряд на Хабре

Средний

12 мин

4.7K

Третья часть материала о CI/CD в котором мы рассмотрим работу с gitlab container registry.

О серии статей

Все найденные мной русскоязычные гайды не дают базового понимания того, как это работает, по большому счету это просто инструкции по настройке, причем под какой-то конкретный продукт и кейс: .net, Java, Node JS, etc.

Целью серии статей является детальное и схематичное описание того, как все это устроено. Главная задача — вооружить читателя фундаментальным пониманием, что конкретно ему требуется сделать в его конкретном случае. Помимо самой инструкции по настройке, это будет так же справочник для погружения в DevOps, охватывающий:

  • Максимально много функционала, которыми обладает Gitlab (общие абстракции актуальны и для его аналогов).

  • Инструменты которые нужны: Bash, Docker, Kubernetes и другие.

  • Общую теорию и практику с конкретными сценариями.

Если проект переходит из стадии разработки в продакшн где начинает осваивать рынок, появляется запрос на бесперебойную работу. Одним из основным требований будет исключить downtime, разгрузить production сервер от лишней нагрузки, организовать процесс оперативной обработки инцидентов, чтобы в случае доставки нестабильного релиза была возможность откатиться.


Материал разбит на несколько частей.

  1. Настройка GitLab CI/CD: понимаем принципы работы и запускаем первый pipeline

  2. Настройка self-hosted gitlab runner (CI/CD)

  3. [Вы здесь → ] Работа с registry (GitLab CI/CD)

  4. [todo] Горизонтальное масштабирование CI/CD (высоко-нагруженный продакшн)

Оглавление

  • Работа с артефактами и registry в .gitlab-ci.yml

  • Gitlab Container Registry

  • Описание gitlab-ci.yml

    • Джоба build-image

    • Джоба test-image

  • Возвращаемся к практике

    • Особенность при запуске gitlab runner в SELinux

  • Проверяем работу

  • Запуск контейнера на прод-сервере


Работа с артефактами и registry в .gitlab-ci.yml

Расширим шаблонный .gitlab-ci.yml с использованием хранилища артефактов. Представим что наше приложение работает в docker контейнере, контейнер запускается на базе образа. Образу можно назначить версию и хранить его в registry. Это позволит хранить историю наших релизов в проде и в случае проблемы откатываться. Еще одним преимуществом будет то, что мы можем запускать сборку и тесты на отдельном сервере, отдельно от production сервера. В таком случае на прод сервере будет достаточно просто загрузить артефакт и запустить его.

У нас есть 2 варианта как мы поступим.

  1. Мы можем создавать артефакт в виде Docker образа и сохранить его в файл. Но это больше подойдет для временного хранения файлов и передачи их между джобами. Если пойти таким путем получиться что-то такое:

    build-image:
      stage: build
      script:
        - docker build -t myapp:latest .
        - docker save myapp:latest -o myapp.tar   # образ → файл
      artifacts:
        paths:
          - myapp.tar
        expire_in: 2 weeks          # будет храниться 2 недели, 
                                    # затем очиститься чтобы не засорять хранилище
    
  2. Gitlab поддерживает специальное хранилище для Docker обзаров: GitLab Container Registry. Именно его мы и будем использовать.

Gitlab Container Registry

Сделаем имитацию нашего приложения в виде bash скрипта который просто выводит какое-то сообщение в консоль:

my-app.sh

#!/bin/bash

echo "Message from my-app.sh v1.0.0: Hello world!"

И добавим простейший Dockerfile для запуска нашего приложения:

Dockerfile

FROM alpine:3.23

# Копируем скрипт в контейнер
COPY my-app.sh /my-app.sh

# Делаем его исполняемым
RUN chmod +x /my-app.sh

# Запускаем скрипт при старте контейнера
CMD ["/my-app.sh"]

Описание gitlab-ci.yml

Теперь опишем .gitlab-ci.yml который будет делать сборку контейнера и проверять его работоспособность в двух разных stages. Так же реализуем автоматическое версионирование наших релизов. Но наша семантика версионирования будет не по стандарту semver, так как semver больше подходит для библиотек. Мы будем использовать в качестве версии дату в формате: YYYY-MM-DD-HH-MM. Именно такой формат удобен тем что он сразу дает понимание о дате релиза и в случае сортировки артефактов по алфавиту сохраняет правильную хронологию.

stages:
  - build
  - test

default:
  image: docker:29.4

build-image:
  stage: build
  script:
    - IMAGE_TAG=$(date +%Y-%m-%d-%H-%M)
    - echo "Версия сборки — $IMAGE_TAG"
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$IMAGE_TAG .
    - docker push $CI_REGISTRY_IMAGE:$IMAGE_TAG
    - docker tag $CI_REGISTRY_IMAGE:$IMAGE_TAG $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest

test-image:
  stage: test
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker run --rm $CI_REGISTRY_IMAGE:latest
Подробный разбор всех команд из script

Джоба build-image

IMAGE_TAG=$(date +%Y-%m-%d-%H-%M)

Определяем переменную IMAGE_TAG, в которой будет храниться дата в формате YYYY-MM-DD-HH-MM.

$(...)

Выполняет команду внутри скобок и подставляет её stdout в качестве значения.

date +%Y-%m-%d-%H-%M

date — утилита вывода времени. Умеет принимать шаблон нужного формата вывода.
+ — указывает что мы хотим именно посмотреть время, а не установить, далее идет шаблонизация.
Используемые “токены” в шаблоне:

  • %Y — год, например 2026

  • %m — месяц, например 04

  • %d — день, например 10

  • %H — часы, например 15

  • %M — минуты, например 42

Возможные токены шаблона и остальные возможности утилиты.

echo "Версия сборки — $IMAGE_TAG"

Выводит сообщение со значением переменной $IMAGE_TAG, которая будет использована для установки тега.

docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

Авторизация в Docker-клиенте с указанием предустановленных переменных (predefined CI/CD variables) в gitlab ci.
docker login — команда входа в Docker registry
-u $CI_REGISTRY_USER — указываем имя пользователя
-p $CI_REGISTRY_PASSWORD — указываем пароль
$CI_REGISTRY — указываем адрес контейнерного registry

Все переменные gitlab сам устанавливает, мы их только читаем.

docker build -t $CI_REGISTRY_IMAGE:$IMAGE_TAG .

docker build -t <...> . — запускаем сборку указывая в качестве build context текущую директорию (точка . на конце указывает на текущую директорию). Build context — это набор всех файлов к которым нужен доступ на этапе сборке. Мы предоставляем полностью всю текущую директорию, но более правильно было бы контролировать какие файлы нужны, а какие следует игнорировать. Сделать это можно через .dockerignore (документация).
-t $CI_REGISTRY_IMAGE:$IMAGE_TAG — задает тег образа в формате имя:тэг.
$CI_REGISTRY_IMAGE — Гитлаб подставит что-то вроде registry.gitlab.com/mygroup/myproject
$IMAGE_TAG — мы определили ранее, получиться например 2026-04-10-15-42
Итого будет подставлено что-то вроде: registry.gitlab.com/mygroup/myproject:2026-04-10-15-42.

docker tag $CI_REGISTRY_IMAGE:$IMAGE_TAG $CI_REGISTRY_IMAGE:latest

Для того же самого локального образа, мы устанавливаем еще один тег :latest. Если мы только что создали например: registry.gitlab.com/mygroup/myproject:2026-04-10-15-42, то мы даем ему так же имя registry.gitlab.com/mygroup/myproject:latest.

docker push $CI_REGISTRY_IMAGE:latest

Публикуем в registry так же наш тег latest. Таким образом в registry есть 1 образ с 2 именами. Когда джоба отработает повторно, она перезапишет указатель latest на более новый образ. Это позволяет нам всегда иметь доступ к последнему артефакту через тег latest. Можно использовать любое название, не обязательно именно latest, но это общепринятый подход нейминга докер образов.

Джоба test-image

docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

В джобе test-image мы снова должны авторизоваться в registry, как изображалось ранее на схеме (в первой части статьи или ниже в текущей), под каждую джобу создается отдельный docker контейнер.

docker run --rm $CI_REGISTRY_IMAGE:latest

docker run — создать и запустить контейнер
--rm — после завершения контейнера автоматически удалить его
$CI_REGISTRY_IMAGE:latest — ссылаемся на тот самый latest который мы создали для удобства

На первый взгляд все логично. Но работать такая конфигурация в рамках shared runners не будет.

На шаге docker build мы получим ошибку:

$ docker build -t $CI_REGISTRY_IMAGE:$IMAGE_TAG .
ERROR: failed to connect to the docker API at tcp://docker:2375: lookup docker on 169.254.169.254:53: no such host

Причина в том что мы используем image: docker, но это не полноценный докер, а лишь его cli клиент который обращается к docker daemon. Именно ошибку обращения к docker daemon мы и видим.

Shared runner не предоставил нам работающий docker daemon, который готов принимать команды от CLI-клиента. Несмотря на то что он есть и он запускает наши джобы.

Если мы хотим использовать docker-cli в наших джобах, то мы вынуждены следовать одному из двух вариантов:

  • Использовать рутовый docker daemon (не dind)

  • Поднимать еще один docker daemon, но конкретно для джоб (dind)

На этом моменте важно понимать, как устроен docker-cli и docker daemon. Есть 2 контейнера:

  • Docker daemon (dind, например образ docker:29-dind)

  • Docker CLI (например образ docker:29-cli или без суффикса cli)

Чтобы CLI-клиент, отправил какую-то команду, ему нужно связаться с docker daemon. Связь может осуществляться 2-мя способами:

  • ip + port

  • docker.sock файл (linux socket — это альтернатива ip + port, но в виде файла)

Но чтобы указать способ связи (ip+port или docker.sock) с docker daemon, нужно изменять конфигурации ранера, что нам недоступно в контексте shared runner. Если мы хотим иметь доступ к конфигурации ранера, менять его executor и прочие параметры, нужно установить свой self-hosted gitlab runner.

Моя инструкция по настройке self-hosted gitlab runner.

На самом деле, есть еще несколько альтернативных путей
  • Использовать kaniko — он был наиболее предпочтительным, так как позволял легко сделать билд без запуска привилегированного режима docker. По каким-то причинам поддержка kaniko прекратилась (issue). По этому его отбрасываем.

  • Использовать buildah, чем-то похоже на kaniko, преподноситься как его альтернатива. Но меня этот вариант тоже отталкивает — судя по issues есть подводные камни которые вынуждают все равно использовать привилегированный режим (что приравнивает его к широко распространенному dind).

Здесь уместно вспомнить схему из первой части данной серии статей.

Эта схема правдива в том случае, если GitLab Runner установлен напрямую на host сервера. Я же предпочитаю поднимать Gitlab Runner в Docker контейнере, как я описывал это в своей инструкции. Теперь актуализируем схему для двух вариантов, которые озвучены выше.

Использование рутового docker daemon (не dind)

Это не docker in docker

Это не docker in docker

В данном случае мы не поднимаем отдельный docker daemon, команды CLI будут направлены к хостовому docker’у, соответственно это будет аффектить рутовую машину. Все сборки будут использовать один общий кэш хоста. Изоляции джобов не будет, это может вызвать race condition (состояние гонки), например если две параллельные джобы попытаются удалить или использовать один и тот же образ.

Но есть плюс — джобы будут быстрее отрабатывать и конфигурация будет проще.

Пример конфигурации `config.toml`
concurrent = 1
check_interval = 0

[[runners]]
  name = "laptop-dc"
  url = "https://gitlab.com/"
  token = "ВАШ_РАННЕР_ТОКЕН_ИЗ_НАСТРОЕК_GITLAB"
  executor = "docker"
  [runners.docker]
    image = "alpine:3.22.4" 
    privileged = false
    disable_entrypoint_overwrite = false
    disable_cache = false
    volumes = [
        "/cache",
        "/var/run/docker.sock:/var/run/docker.sock"
    ]

Главное здесь: "/var/run/docker.sock:/var/run/docker.sock".

Такой способ с отсутствием изоляции меня не устраивает, как и большинство разработчиков. По этой причине рассмотрим следующую схему, именно ее мы и реализуем на практике.

Использование отдельного docker daemon, но конкретно для джоб (dind)

Docker in docker (dind)

Docker in docker (dind)

В данном варианте в целях изоляции запускается внутренний docker daemon, рутовый docker daemon и его контейнеры живут отдельно. Может показаться, что с точки зрения безопасности этот вариант предпочтительнее, но это не совсем так. Чтобы поднять полноценный docker daemon внутри контейнера, необходимо запускать контейнер в привилегированном режиме.

Впрочем, расшаривать соединение с основным docker daemon через тот же docker.sock файл — та же самая брешь в безопасности. Тот, кто забрался внутрь пайплайна, сможет забраться внутрь нашего сервера (container escape подробно на анг.). Дополнительным слоем безопасности будет только переход на podman. Подробнее я писал в статье: “Root в контейнере — это root на хосте? Разбираю важные особенности прав доступов в контейнерах Docker/Podman”


Возвращаемся к практике

Чтобы иметь больше возможностей и был доступ к более тонкой конфигурации, мы запустили собственный раннер на своем сервере. Он будет запускать весь пайплайн.

После готовности нашего runnera и успешного его коннекта с gitlab.com пробуем запустить пайплайн, но так, что бы его выполнял наш self hosted runner. Для этого должен совпадать тег указанный в раннере. В инструкции я использовал тег laptop-dc. Поэтому в gitlab-ci.yml должен фигурировать этот тег для джобов.

В gitlab-ci.yml, к блоку default, нужно добавить вспомогательный сервис с docker daemon:

default:
  image: docker:29.4-cli
  services:
    - docker:29.4-dind

Теперь, чтобы cli и dind смогли поимать соединение друг с другом, нужно указать, по какому адресу следует подключаться:

Перед блоком default, добавляем 2 переменные:

variables:
  DOCKER_HOST: tcp://docker:2375
  DOCKER_TLS_CERTDIR: ""
Итоговая конфигурация gitlab-ci.yml
stages:
  - build
  - test

variables:
  DOCKER_HOST: tcp://docker:2375
  DOCKER_TLS_CERTDIR: ""

default:
  image: docker:29.5.3-cli
  services:
    - docker:29.5.3-dind

build-image:
  tags:
    - laptop-dc
  stage: build
  script:
    - IMAGE_TAG=$(date +%Y-%m-%d-%H-%M)
    - echo "Версия сборки — $IMAGE_TAG"
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$IMAGE_TAG .
    - docker push $CI_REGISTRY_IMAGE:$IMAGE_TAG
    - docker tag $CI_REGISTRY_IMAGE:$IMAGE_TAG $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest

test-image:
  tags:
    - laptop-dc
  stage: test
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker run --rm $CI_REGISTRY_IMAGE:latest

Не забываем что нужно отредактировать Runner конфиг (config.toml) на возможность запуска dind выставив привилегированный режим:

concurrent = 1
check_interval = 0

[[runners]]
  name = "laptop-dc"
  url = "https://gitlab.com/"
  token = "ВАШ_ТОКЕН"
  executor = "docker"
  [runners.docker]
    image = "alpine:3.22.4" 
    privileged = true
    tls_verify = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache"]

Основная правка:

    privileged = true
    tls_verify = false

Особенность при запуске gitlab runner в SELinux

Если у вас обычный ubuntu (не SELinux), то вам достаточно иметь такой volume: /var/run/docker.sock:/var/run/docker.sock

Но мой случай сложнее, так как я запускаю gitlab runner в контейнере podman на fedora (SELinux). В моем случае, docker.sock отсутствует и вместо него нужно пробрасывать такой волум:

 - /run/user/1000/podman/podman.sock:/var/run/docker.sock:z

Где 1000 это user id под которым я запускаю контейнер с раннером (получить можно командой id -u).

Но и этого недостаточно, так как в пайплайне мы получим ошибку еще до того, как начнут выполнять команды из наших джобов:

ERROR: Preparation failed: getting docker info: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.51/info": dial unix /var/run/docker.sock: connect: permission denied (docker.go:1218:0s)

Это связано с тем, что docker executor от gitlab-runner, не может подготовить контейнер внутри которого будет запущена джоба. Причина — нет прав на подключение. Посмотрим записи ли записи в журнале политик доступа SELinux.

sudo ausearch -m avc

В выводе видим:

type=AVC msg=audit(1780685305.517:1634): avc:  denied  { connectto } for  pid=3001141 comm="gitlab-runner" path="/run/user/1000/podman/podman.sock" scontext=system_u:system_r:container_t:s0:c635,c686 tcontext=unconfined_u:unconfined_r:container_runtime_t:s0-s0:c0.c1023 tclass=unix_stream_socket permissive=0

Пробуем включить режим Permissive (без запретов, но с логированием не разрешенных действий).

gtosss@laptop-dc:~/inf-config$ getenforce
Enforcing
gtosss@laptop-dc:~/inf-config$ sudo setenforce 0
gtosss@laptop-dc:~/inf-config$ getenforce
Permissive

В UI gitlab перезапускаем пайплайн и видим, что проблема решилась.

Теперь нужно расширить политики доступа, чтобы можно было вернуться в Enforcing режим.

Я записал строку с отказом, которую получил в sudo ausearch -m avc, в файл audit.log.

Устанавливаем необходимые исходники политик для генерации нашего модуля:

sudo yum install selinux-policy-devel

Теперь генерируем политику:

audit2allow -i audit.log -R > gitlab_runner_connect_for_dind.te

Получился такой результат:

require {
        type container_t;
}

#============= container_t ==============
container_stream_connect(container_t)
Как узнать, что такое container_stream_connect

Утилита macro-expander позволяет сгенерировать на основе шаблона конкретные разрешения container_stream_connect:

macro-expander "container_stream_connect(container_t)" > macros_output.te

Теперь в файле macros_output.te можно ознакомиться со всеми allow правилами.

На первой строке допишем название модуля итого получим:

policy_module(gitlab_runner_connect_for_dind, 1.0)

require {
        type container_t;
}

#============= container_t ==============
container_stream_connect(container_t)

Компилируем через make:

make -f /usr/share/selinux/devel/Makefile gitlab_runner_connect_for_dind.pp

И устанавливаем:

sudo semodule -i gitlab_runner_connect_for_dind.pp

Так можно убедиться, что модуль установлен:

sudo semodule -l | grep gitlab_runner_connect_for_dind

Теперь можно вернуться в безопасный режим:

sudo setenforce 1

Перезапускаю пайплайн и все работает, значит политика работает.

Проверяем работу

Я запустил пайплайн 2 раза. Поэтому я ожидаю 3 контейнера в Gitlab Container Registry: 2 с конкретным тегом и 1 с тегом latest.

Теперь мы можем запустить образ где угодно, копируем путь кликом по соответствующей иконке.

Путь к моему контейнеру выглядит так:

registry.gitlab.com/webstap/self-server-config:2026-06-05-22-07

Запуск контейнера на прод-сервере

Напишем простейший bash скрипт для авторизации в gitlab docker registry, назовем его docker-login-gitlab-registry.bash:

#!/usr/bin/env bash

echo "$CI_DEPLOY_PASSWORD" | docker login $CI_REGISTRY -u $CI_DEPLOY_USER --password-stdin

Теперь нам нужно получить пользователя и пароль для авторизации. Заходим в UI Gitlab.

Нам отобразится пользователь и пароль, с помощью которых можно получить доступ к registry, запишем их в файл variables.sh.

export CI_REGISTRY="registry.gitlab.com"
export CI_DEPLOY_USER="<ПОЛУЧЕННЫЙ_DEPLOY_USER>"
export CI_DEPLOY_PASSWORD="ПОЛУЧЕННЫЙ_DEPLOY_PASSWORD"

Оба файла положим в директорию, например user-gitlab-registry. Теперь достаточно перейти в эту директорию через cd и выполнить команду:

source variables.sh

Эта команда установит все переменные окружения в текущий shell process. Теперь достаточно запустить docker-login-gitlab-registry.bash:

./docker-login-gitlab-registry.bash

Пробуем запустить образ:

docker run registry.gitlab.com/webstap/self-server-config:2026-06-05-22-07
$ docker run registry.gitlab.com/webstap/self-server-config:2026-06-05-22-07
Unable to find image 'registry.gitlab.com/webstap/self-server-config:2026-06-05-22-07' locally
2026-06-05-22-07: Pulling from webstap/self-server-config
11ab09ea930b: Pull complete 
a9b65603287c: Pull complete 
Digest: sha256:fecc87f73faf11a953caa6479490c8551727b49c0eff024d65080b1a36d87fc0
Status: Downloaded newer image for registry.gitlab.com/webstap/self-server-config:2026-06-05-22-07
Message from my-app.sh v1.0.0: Hello world!

Спасибо, что дочитали!

Кто уже подписан на телеграм-канал — отдельная благодарность. Помимо технических статей поднимаем и социально важные темы.

Если материал полезен и изложен понятным языком — не стесняйтесь ставить плюсы. Это поможет выделиться на фоне остальных статей, так же это хороший способ поддержать меня.

С вами был Тимофей. Кто я?

Разрабатываю с 2015 года. Стартовал как front-end разработчик на React, после 6-лет переключился на full-stack, последние годы — чаще DevOps. Мой публичный WakaTime.