В этом уроке разберём, как jenkins docker agent превращает образ в одноразовую среду сборки, чем agent docker отличается от agent dockerfile, как пробросить кеш зависимостей и где разложены классические грабли с правами и сокетом.
Механика: как jenkins docker agent запускает стейдж в контейнере
Идея простая. Ты указываешь Jenkins образ, он делает примерно docker run, монтирует внутрь workspace с твоим кодом и выполняет шаги стейджа уже внутри контейнера. Закончился стейдж - контейнер удаляется. Никаких следов, никакого мусора на хосте. Воспроизводимость берётся из того, что образ один и тот же у всех: на твоей машине, на агенте, у соседа.
Минимальный рабочий пример. Берём официальный образ Maven с нужной Java и собираем проект:
Код: Выделить всё
pipeline {
agent {
docker { image 'maven:3.9.9-eclipse-temurin-21' }
}
stages {
stage('Build') {
steps {
sh 'mvn -B -ntp clean verify'
}
}
}
}
Код: Выделить всё
pipeline {
agent none
stages {
stage('Frontend') {
agent { docker { image 'node:22-alpine' } }
steps {
sh 'npm ci'
sh 'npm run build'
}
}
stage('Backend') {
agent { docker { image 'golang:1.23-alpine' } }
steps {
sh 'go build ./...'
}
}
}
}

args, reuseNode и кеш: jenkins docker на практике
Голый docker { image } работает, но в реальной жизни нужны нюансы. Самый частый - кеш зависимостей. Каждый новый контейнер стартует чистым, и если ничего не сделать, npm ci или mvn будут качать полмира заново при каждой сборке. Лечится монтированием через args - это строка, которая дословно уходит в docker run:
Код: Выделить всё
pipeline {
agent {
docker {
image 'maven:3.9.9-eclipse-temurin-21'
args '-v $HOME/.m2:/root/.m2'
}
}
stages {
stage('Build') {
steps {
sh 'mvn -B -ntp clean verify'
}
}
}
}
Второй важный флаг - reuseNode. По умолчанию, когда ты вешаешь docker-агент на отдельный стейдж внутри pipeline с обычным агентом наверху, Jenkins может выделить новый узел и новый workspace. reuseNode true говорит: "не плоди узлы, смонтируй текущий workspace с текущего агента внутрь контейнера". Это критично, когда стейджи передают друг другу артефакты по файловой системе:
Код: Выделить всё
pipeline {
agent { label 'linux' }
stages {
stage('Test') {
agent {
docker {
image 'python:3.13-slim'
reuseNode true
}
}
steps {
sh 'pip install -r requirements.txt && pytest'
}
}
}
}
Готовый образ с Docker Hub - не всегда то, что нужно. Иногда твоя сборка требует специфический набор: тот же Node, но плюс ffmpeg, плюс пара системных пакетов, плюс твой внутренний CLI. Держать это в публичном образе глупо, тащить руками - неповторяемо. Решение - положить Dockerfile прямо в репозиторий и собирать образ агента из него:
Код: Выделить всё
pipeline {
agent {
dockerfile {
filename 'Dockerfile.ci'
dir 'build'
additionalBuildArgs '--build-arg NODE_VERSION=22'
}
}
stages {
stage('Build') {
steps {
sh 'make build'
}
}
}
}
Требования и типичные грабли
Чтобы всё это работало, на агенте Jenkins должен быть установлен Docker, а пользователь, под которым крутится Jenkins, обязан иметь доступ к Docker-демону. На этом и спотыкается большинство.
- Permission denied на сокете. Классика: "Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock". Пользователь jenkins не в группе docker. Лечится добавлением в группу (usermod -aG docker jenkins) и перезапуском службы. Помни: членство в группе docker фактически равно root на этом хосте, относись к этому как к решению по безопасности, а не как к галочке.
- Файлы принадлежат root. Если внутри контейнера процесс работает от root, созданные в workspace файлы будут с владельцем root, и следующий стейдж или чистка workspace упадёт на правах. Лечится прокидыванием UID/GID через args, например -u 1000:1000, либо образами, которые умеют работать не от рута.
- Docker-in-Docker и сокет хоста. Если стейджу нужно собирать сам Docker-образ, не лезь в настоящий DinD без острой необходимости - чаще пробрасывают сокет хоста через args (-v /var/run/docker.sock:/var/run/docker.sock). Это удобно, но контейнер получает полный контроль над демоном хоста. Для изолированных сборок образов разумнее посмотреть в сторону инструментов, не требующих демона.
- Кеш не подхватывается. Смонтировал том, но кеша нет? Проверь, что путь внутри контейнера совпадает с тем, куда тулчейн реально пишет (у root это /root/.m2, у непривилегированного пользователя - другой $HOME).
Мини-лаба
Повтори руками:
- Сделай простейший Jenkinsfile с agent { docker { image 'node:22-alpine' } } и шагом sh 'node --version'. Убедись, что версия Node берётся из образа, а не с агента.
- Добавь второй стейдж с другим образом (python:3.13-slim) и agent none наверху. Посмотри в логе, как поднимаются два разных контейнера.
- Подключи кеш через args '-v $HOME/.npm:/root/.npm', запусти npm ci дважды и сравни время второй сборки.
- Положи в репозиторий Dockerfile.ci с парой extra-пакетов и переключись на agent { dockerfile { filename 'Dockerfile.ci' } }.
- Специально сломай права: запусти контейнер от root, создай файл в workspace, затем добавь -u 1000:1000 и сравни владельца файлов.
- Зачем нужен agent none на верхнем уровне pipeline, если у каждого стейджа свой docker-агент?
- Чем отличается agent { docker } от agent { dockerfile } и когда выбирать второе?
- Что делает reuseNode true и какую проблему он решает при передаче артефактов между стейджами?
- Почему проброс /var/run/docker.sock внутрь контейнера - это удобно, но опасно с точки зрения безопасности?
Docker-агенты убирают зоопарк инструментов с агента и дают каждой сборке чистое, версионируемое окружение. agent { docker { image } } - быстрый способ взять готовый образ, agent { dockerfile } - собрать свой прямо из репозитория. args монтирует кеш и крутит параметры docker run, reuseNode держит стейджи в одном workspace. А все грабли сводятся к двум словам - права и сокет: дай Jenkins доступ к Docker-демону осознанно и следи за владельцем файлов. Это и есть современный, воспроизводимый CI без сюрпризов "у меня собиралось".