Знакомая боль: приложение готово, тесты зелёные, а на сервер оно едет "руками". Кто-то заходит по ssh, делает docker build, забывает поставить тег, пушит не туда, и в проде оказывается образ, который никто не может воспроизвести. Через неделю "а какой коммит там собран" - ответа нет.
Связка jenkins docker эту дыру закрывает. Идея простая: образ собирается ровно там же, где прошли тесты, тегается понятным идентификатором (номер сборки, хеш коммита), сканируется на дыры и публикуется в registry. Один и тот же артефакт едет дальше по конвейеру. В этом уроке разберём, как настроить docker jenkins так, чтобы jenkins docker build и jenkins docker push были обычными шагами пайплайна, а не ручным ритуалом.
Сразу про версии, чтобы не строить на песке. Актуальная LTS на лето 2026 - линия 2.541.x (релиз 2.541.1 от 21 января 2026), и это последняя ветка с поддержкой Java 11. Следующая LTS, которая выходит 15 апреля 2026, требует уже Java 21, а weekly-сборки давно на Java 21. Если ставишь новый контроллер - сразу бери Java 21, чтобы не упереться в обновление через пару месяцев. Blue Ocean из книги Ластера трогать не будем: проект фактически заброшен, новый UI Jenkins живёт своей жизнью, и завязываться на Blue Ocean в 2026 не стоит.

Глобальная переменная docker: build, image, withRegistry
Классический путь - плагин Docker Pipeline. Он добавляет глобальную переменную docker с тремя рабочими лошадками:
- docker.build(tag, context) - собирает образ из Dockerfile, возвращает объект образа.
- docker.image(ref) - оборачивает существующий образ, чтобы его запускать (.inside, .run, .withRun).
- docker.withRegistry(url, credsId) - логинится в registry на время блока и пушит туда.
Код: Выделить всё
pipeline {
agent any
environment {
REGISTRY = 'registry.cyberlake.ru'
IMAGE = 'team/web-app'
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Build image') {
steps {
script {
// тег по номеру сборки + короткий хеш коммита
def gitSha = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
env.IMG_TAG = "${env.BUILD_NUMBER}-${gitSha}"
app = docker.build("${REGISTRY}/${IMAGE}:${IMG_TAG}", "-f Dockerfile .")
}
}
}
stage('Test in container') {
steps {
script {
// поднимаем собранный образ и прогоняем тесты внутри него
app.inside {
sh 'pytest -q'
}
}
}
}
stage('Push') {
steps {
script {
docker.withRegistry("https://${REGISTRY}", 'registry-creds') {
app.push("${IMG_TAG}")
// двигаем latest только с основной ветки
if (env.BRANCH_NAME == 'main') {
app.push('latest')
}
}
}
}
}
}
}
Если хочется обойтись без script-блока, шаг docker. живёт в Groovy, поэтому в declarative его оборачивают в script - это нормально и не считается грязью.
Сборка без docker-демона: kaniko в Kubernetes
Подход выше требует docker-демона на агенте. В Kubernetes это боль и риск: монтировать /var/run/docker.sock в pod - значит дать сборке root-доступ ко всему хосту, а DinD с privileged - ещё хуже. Поэтому в 2026 на эфемерных агентах в k8s docker build почти не делают. Берут rootless-сборщики: kaniko или buildah. Они собирают образ из Dockerfile целиком в userspace, без демона.
Самый ходовой - kaniko. Через kubernetes-плагин описываем pod с двумя контейнерами: один для сборки приложения, второй - kaniko-executor. Важная деталь из практики: бери образ kaniko с тегом debug, потому что в нём есть busybox-shell (/busybox/sh), а в обычном latest шелла нет, и container-шаг просто не запустится.
Код: Выделить всё
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:debug
command: ["sleep"]
args: ["9999999"]
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker
volumes:
- name: docker-config
projected:
sources:
- secret:
name: registry-creds
items:
- key: .dockerconfigjson
path: config.json
'''
}
}
stages {
stage('Build & push') {
steps {
container('kaniko') {
sh '''
/kaniko/executor \
--dockerfile=Dockerfile \
--context=`pwd` \
--destination=registry.cyberlake.ru/team/web-app:${BUILD_NUMBER} \
--cache=true
'''
}
}
}
}
}
Сканирование образа на уязвимости (Trivy)
Собрать и запушить мало - образ с дырявой базой и кучей CVE отдавать в прод нельзя. Шаг сканирования встраивается между build и push, чтобы дырявый образ просто не доехал до реестра. Удобнее всего гонять Trivy из официального образа, ничего не ставя на агент:
Код: Выделить всё
stage('Scan') {
steps {
sh """
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image \
--exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
${REGISTRY}/${IMAGE}:${IMG_TAG}
"""
}
}
Типичные грабли
- Сокет docker как root. Монтировать /var/run/docker.sock или гнать privileged DinD на общих агентах - дыра в безопасности. В k8s переходи на kaniko/buildah.
- kaniko:latest без шелла. container('kaniko') падает, потому что нет /busybox/sh. Нужен тег debug.
- Тег latest на всё подряд. Каждая ветка перетирает latest, и непонятно, что в реестре. Тегай по сборке и коммиту, latest двигай только с main.
- Креды в открытую. Никаких паролей реестра в Jenkinsfile. Только credentials-биндинг или k8s-секрет.
- Trivy без exit-code. Скан, который только печатает и не валит билд, никого ни от чего не защищает.
Полезно держать в голове соседей. В GitLab CI и GitHub Actions сборка образа - это, как правило, готовый job/action (docker buildx, kaniko-step, аналог aquasecurity/trivy-action), описанный в YAML, и раннеры эфемерны по умолчанию. Jenkins даёт больше контроля и гибкости (особенно сложные multibranch-конвейеры и Groovy-логика), но требует, чтобы ты сам держал контроллер, агентов и плагины. Связка docker jenkins сильна там, где нужна нетривиальная оркестрация и self-hosted-инфраструктура; для простого "собрал-запушил" облачные системы зачастую короче. Знать оба подхода - значит выбирать инструмент под задачу, а не под привычку.
Мини-лаба
- Возьми любое приложение с Dockerfile, собери его через docker.build с тегом ${BUILD_NUMBER}-${gitSha}.
- Заведи в Jenkins секрет registry-creds и запушь образ через docker.withRegistry в свой реестр (подойдёт локальный registry:2).
- Добавь stage с app.inside и прогони внутри образа хотя бы один тест.
- Встрой Trivy-скан с --exit-code 1 --severity HIGH,CRITICAL и убедись, что дырявый образ валит сборку.
- Со звёздочкой: перепиши пайплайн на kubernetes-агент с kaniko:debug и k8s-секретом, без docker-демона.
- Чем docker.build отличается от docker.image и когда нужен каждый?
- Почему в Kubernetes предпочитают kaniko/buildah вместо docker build с сокетом или DinD?
- Как withRegistry получает доступ к реестру и где должны лежать креды?
- Что делает --exit-code 1 в Trivy и почему без него скан почти бесполезен?
Образ должен собираться, проверяться и публиковаться из конвейера, а не руками. Глобальная переменная docker (build, image, withRegistry) закрывает классический сценарий с демоном, kaniko - современный rootless-путь для эфемерных агентов в Kubernetes, а Trivy не пускает дырявый образ в реестр. Тегируй осмысленно, прячь креды, вали билд на критичных CVE - и docker jenkins из источника боли превращается в предсказуемый поток артефактов.