Aller au contenu principal
JenkinsCI/CDDevOps

Jenkins #5 — Jenkins et Docker : Build d'Images, DinD et Kaniko

30 min de lecture Jenkins — Chapitre 5

Maîtrise l'intégration Docker dans Jenkins : build d'images, Docker agent, Docker-in-Docker, Kaniko et les patterns de production.

Docker et Jenkins forment le duo le plus courant en CI/CD. Environnements reproductibles, isolation entre builds, agents éphémères — Docker résout les problèmes historiques de Jenkins : conflits de dépendances, pollution entre projets, et le fameux “ça marchait sur ma machine”.

Ce cours couvre trois axes : utiliser Docker comme agent de build, builder et publier des images, et choisir la bonne stratégie entre DinD, socket bind et Kaniko.

Docker comme agent de build

Le cas le plus fréquent : exécuter ton pipeline dans un conteneur éphémère. Jenkins pull l’image, monte le workspace en volume, exécute les steps, puis détruit le conteneur. Chaque build repart d’un environnement propre.

pipeline {
    agent {
        docker {
            image 'node:20-alpine'
            args '-v /tmp/.npm-cache:/root/.npm -e NODE_ENV=test --memory=2g'
            reuseNode true
        }
    }
    stages {
        stage('Build') { steps { sh 'npm ci && npm run build' } }
        stage('Test')  { steps { sh 'npm test' } }
    }
}

Tu peux utiliser des agents différents par stage — frontend en Node, backend en Go — puis rassembler les artefacts avec stash/unstash :

pipeline {
    agent none
    stages {
        stage('Frontend') {
            agent { docker { image 'node:20-alpine' } }
            steps {
                sh 'cd frontend && npm ci && npm run build'
                stash includes: 'frontend/dist/**', name: 'front'
            }
        }
        stage('Backend') {
            agent { docker { image 'golang:1.22-alpine' } }
            steps {
                sh 'cd backend && go build -o app .'
                stash includes: 'backend/app', name: 'back'
            }
        }
        stage('Package') {
            agent any
            steps {
                unstash 'front'
                unstash 'back'
                sh 'tar czf release.tar.gz frontend/dist backend/app'
            }
        }
    }
}

💡 Agent Dockerfile : au lieu d’une image publique, pointe vers un Dockerfile du repo avec agent { dockerfile { filename 'ci/Dockerfile.ci' } }. Idéal pour un agent custom avec Chromium ou des CLIs pré-installés.

⚠️ Piège du reuseNode : sans reuseNode true, chaque stage Docker spawne un nouveau conteneur et perd le workspace. Ajoute-le systématiquement quand tu veux partager des fichiers entre stages sur le même agent.

Builder et pusher des images

Le workflow complet pour builder, scanner et publier des images Docker. Le plugin docker-workflow fournit docker.build() et docker.withRegistry() :

pipeline {
    agent any
    environment {
        REGISTRY = 'registry.example.com'
        IMAGE = 'mon-app'
        TAG = "${BUILD_NUMBER}-${GIT_COMMIT.take(7)}"
    }
    stages {
        stage('Build & Push') {
            steps {
                script {
                    docker.withRegistry("https://${REGISTRY}", 'registry-creds') {
                        def img = docker.build("${REGISTRY}/${IMAGE}:${TAG}",
                            "--target production .")
                        img.push()
                        img.push('latest')
                    }
                }
            }
        }
        stage('Security Scan') {
            steps {
                sh "docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
                    aquasec/trivy:latest image --severity CRITICAL \
                    --exit-code 1 ${REGISTRY}/${IMAGE}:${TAG}"
            }
        }
    }
    post { always { sh "docker rmi ${REGISTRY}/${IMAGE}:${TAG} || true" } }
}

🔥 Scan de vulnérabilités : Trivy ou Grype avant le push — non-négociable en production. Un --exit-code 1 avec --severity CRITICAL bloque automatiquement le déploiement d’images vulnérables. Ça prend 30 secondes et ça peut sauver ta prod.

🎯 Multi-stage builds : une image avec les devDependencies pèse 500 MB. Avec --target production et un Dockerfile multi-stage bien écrit, elle tombe à 80 MB. Le flag --target est ton meilleur ami pour des images légères.

DinD vs Socket Bind vs Kaniko

Quand Jenkins tourne dans Docker ou sur Kubernetes, comment builder des images Docker ? Trois stratégies, chacune avec ses compromis.

Socket bind — simple mais risqué

Monte le socket Docker du host dans le conteneur Jenkins. Simple, performant, cache partagé. Mais l’accès au socket équivaut à un accès root sur le host — acceptable en dev, interdit en production partagée.

# Le conteneur Jenkins partage le Docker du host
docker run -d --name jenkins \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v jenkins_home:/var/jenkins_home \
    -p 8080:8080 jenkins/jenkins:lts-jdk17

DinD — isolé mais complexe

Un daemon Docker dédié tourne dans un conteneur docker:dind. Isolation complète, mais nécessite privileged: true et perd le cache partagé. Chaque instance Jenkins a son propre Docker — pas de fuite entre projets.

Kaniko — la solution Kubernetes

Kaniko builde des images sans daemon Docker. Il parse le Dockerfile, exécute chaque instruction en userspace, et push directement vers le registry. Pas de socket, pas de privileged — parfait pour Kubernetes.

pipeline {
    agent {
        kubernetes {
            yaml '''
            apiVersion: v1
            kind: Pod
            spec:
              containers:
              - name: kaniko
                image: gcr.io/kaniko-project/executor:debug
                command: ['sleep', 'infinity']
                volumeMounts:
                - name: docker-config
                  mountPath: /kaniko/.docker
              volumes:
              - name: docker-config
                secret:
                  secretName: docker-registry-creds
                  items:
                  - key: .dockerconfigjson
                    path: config.json
            '''
        }
    }
    stages {
        stage('Build & Push') {
            steps {
                container('kaniko') {
                    sh """/kaniko/executor \
                        --context=\$(pwd) \
                        --dockerfile=Dockerfile \
                        --destination=${REGISTRY}/${IMAGE}:${TAG} \
                        --cache=true \
                        --cache-repo=${REGISTRY}/${IMAGE}/cache"""
                }
            }
        }
    }
}

💡 Le flag --cache=true stocke les layers dans un repo de cache sur le registry. Sans lui, chaque build reconstruit toutes les layers. Sur un Dockerfile avec des dépendances lourdes, c’est la différence entre 2 minutes et 15 minutes.

Cas entreprise — choisir sa stratégie

Dans une équipe de 20+ devs avec des microservices conteneurisés, le choix de la stratégie Docker dépend de l’infrastructure existante :

Jenkins sur VM : socket bind en dev local, DinD avec TLS en staging/prod. Le docker-compose ci-dessous illustre un setup DinD sécurisé :

# docker-compose.yml — Jenkins + DinD avec TLS
services:
  jenkins:
    image: jenkins/jenkins:lts-jdk17
    environment:
      DOCKER_HOST: tcp://docker:2376
      DOCKER_TLS_VERIFY: "1"
      DOCKER_CERT_PATH: /certs/client
    volumes:
      - jenkins_home:/var/jenkins_home
      - docker-certs:/certs/client:ro
  docker:
    image: docker:24-dind
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: /certs
    volumes:
      - docker-certs:/certs/client
      - jenkins_home:/var/jenkins_home

Jenkins sur Kubernetes : Kaniko sans hésiter. Pas de conteneur privileged, pas de daemon à gérer, scaling naturel avec les pods éphémères.

🔥 Pattern de nettoyage automatique : planifie un job Jenkins hebdomadaire avec docker image prune -a --filter "until=168h" -f et docker builder prune -f --keep-storage=10GB. Sans ça, le disque de ton agent Jenkins explose en quelques semaines.

Pièges courants et bonnes pratiques

Les erreurs classiques en Jenkins + Docker, vues dans toutes les équipes qui débutent :

⚠️ Pas de nettoyage d’images — chaque build laisse une image derrière lui. En 200 builds, c’est 50 GB de disque. Toujours un docker rmi en post { always {} }.

⚠️ Socket bind en production — un conteneur avec accès au socket Docker peut lancer docker run --privileged et obtenir un shell root sur le host. En prod partagée, c’est un incident de sécurité qui attend de se produire.

⚠️ Ignorer les multi-stage builds — une image de dev avec Node, des outils de build et des devDependencies pèse 800 MB. Une image de production bien découpée fait 50-100 MB. L’impact sur le temps de déploiement est massif.

⚠️ Pas de tag déterministe — utiliser latest en CI, c’est jouer à la roulette russe. Tague avec BUILD_NUMBER-GIT_SHA pour savoir exactement quelle version tourne en production.

🎯 Checklist avant de valider ton pipeline Docker :

  • Image de base minimale (alpine, distroless)
  • Multi-stage build avec --target
  • Scan Trivy/Grype avant le push
  • Nettoyage en post { always {} }
  • Credentials via docker.withRegistry(), jamais en clair

Résumé

Docker dans Jenkins se résume à trois décisions clés :

Agent Docker pour des builds reproductibles. Utilise agent { docker {} } pour les projets simples, agent { dockerfile {} } quand tu as besoin d’un environnement custom.

Build d’images avec docker.build() et docker.withRegistry(). Tag déterministe, scan de sécurité, nettoyage systématique — ces trois éléments transforment un pipeline basique en pipeline de production.

Stratégie de daemon : socket bind en dev, DinD pour l’isolation, Kaniko sur Kubernetes. En 2025, si tu déploies Jenkins sur K8s, Kaniko est la recommandation par défaut. Zéro privileged, zéro daemon, et ça marche.

🖥️ Pratique sur ton propre serveur

Pour suivre Jenkins en conditions réelles, tu as besoin d'un VPS. DigitalOcean offre 200$ de crédit gratuit pour démarrer.

Obtenir 200$

Articles liés