Intermediate

Running Jenkins in Docker

running · jenkins · docker · ci/cd · git · devops · image · agent · dockerfile · jenkinsfile · container · images · dotnetcore · agents · .net · core · multi-architecture · upgrade · buil...

Table of Contents

  1. Course presentation
  2. Run Jenkins in Docker
  1. Create a Jenkins build farm with Docker
  1. Working with Multi Architecture Containers in Jenkins
  1. Maintain your build farm

1. Course presentation

Jenkins is one of the most popular build servers, and Docker represents the new frontier of application development and delivery. Used together effectively, these two tools reinforce each other.

Topics covered in this course

  • Run the Jenkins master in Docker and isolate the state of this master from the container
  • Build Docker images in Docker containers with Jenkins (Docker-in-Docker)
  • Build for CPU architectures you don’t have with multi-architecture builds

Prerequisites

Before starting this course, you should be familiar with:

  • Jenkins and build engineering
  • Docker (basics)

2. Run Jenkins in Docker

Section duration: 32m 23s

2.1 Introduction

The goal of this section is to quickly dive into the hands-on demonstrations. However, before that, it is essential to understand some fundamental aspects of Docker. Knowledge of Jenkins is assumed to be acquired; what matters here is mastery of Docker, particularly for performance, security and portability considerations.

In practice, you can work with Docker for a long time without really understanding what’s going on beneath the surface. This course fixes that right from the start.


2.2 Docker and the Kernel

The first fundamental concept to master is the difference between virtual machines (VMs) and Docker containers.

Classic interview question: What is the fundamental difference between a virtual machine and a container? Expected answer: The kernel (kernel).

  • Each VM has its own individual kernel.
  • All containers share a single kernel.

This is the fundamental truth of a container: sharing a kernel in resource isolation mode.

In this course, we will run a Windows operating system with an NT kernel, then launch a Linux container (based on Debian 9), all on that same NT kernel. How is this possible? This is precisely what we are going to explore.


2.3 Demo: A Linux container on Windows 10

In this demonstration, we start from a Windows shell to prove that we are indeed running Windows 10. We then launch a Jenkins Linux container and open a command line in this container.

Steps completed:

  1. In a Windows shell, the systeminfo command displays information confirming a Windows build (NT kernel).
  2. Docker Desktop is used to launch the Jenkins LTS container.
  3. A shell window is open in the container.
  4. In the container, navigate to /etc and read the os-release file:
# Dans le conteneur Jenkins
cd /etc
cat os-release

This file shows that the system is Linux (Debian), while the host machine is Windows.

Conclusion: Both representations are true. The host says Windows, the container says Linux. Both are right. This is the magic of kernel virtualization.


2.4 What is a Kernel?

The classic definition of the kernel is:

A kernel is a privileged abstraction layer that exists between applications and hardware.

This definition, although true, can be confusing with other layers like:

  • HAL (Hardware Abstraction Layer) — this is partly correct
  • BIOS — no, although it loosely satisfies the definition
  • Device drivers — also in this gray area

The real key to understanding: If you have kernel level access, you have access to everything. The kernel is what has full access and privilege over the system.

The three types of kernels

TypeExampleDescription
MonokernelLinuxAll OS code runs in the same memory space
MicrokernelMinixOnly the minimum runs in privileged mode
Hybrid KernelWindows (NT)Combination of the two approaches

Role of the kernel

The kernel manages:

  • Memory and Processes
  • An abstraction layer on hardware for applications above

Example: When an application tries to communicate over the Internet, it calls through its software stack, down into the OS and into the kernel, which translates these calls into system calls to the network hardware. The same goes for loading a file from a hard drive.


2.5 Why it matters: Running Linux containers on Windows

A preconceived idea to discard: it is not simply a Linux shell (bash) running on an NT kernel. This would be simulated Linux. That’s not what’s happening.

What actually happens: We run a Linux container on a Linux VM, itself hosted by Hyper-V, which runs on an NT kernel (Windows).

Application (Jenkins)
       ↓
Conteneur Linux (Debian)
       ↓
VM Linux (kernel Linux)
       ↓
Hyper-V (Windows)
       ↓
Kernel NT (Windows)
       ↓
Matériel physique

So in this hybrid Windows configuration, our Linux container does not share the NT kernel with the host OS. It shares the kernel of the Linux VM.

Note: The standard definition of container (sharing the same kernel) is not entirely accurate in this Windows/Hyper-V case.

Important Considerations

Performance: According to a 2014 IBM research article (“An Updated Performance Comparison of Virtual Machines and Linux Containers”), containers provide equal to or better performance than VMs in almost all cases. The Linux in Windows approach adds an extra layer, so running Linux containers on a native Linux platform will always perform better.

Security: Container security considerations relate to the vulnerability (or lack thereof) of the kernel and shared OS. An attacker who compromises the kernel has access to all containers that share that kernel. This is a fundamental difference with VMs.

Portability: If you are building for production deployment on Linux (which is likely), develop on Linux to avoid surprises.


2.6 The vision and the why: Jenkins on Docker

To illustrate the vision, let’s compare with Microsoft’s Azure build system.

Azure DevOps build system:

  1. A build is queued
  2. Azure reads the build definition and looks for requirements for agents (OS, dependencies, etc.)
  3. Azure matches the build with an agent
  4. Azure launches a matching VM to run the build
  5. A second build in parallel launches a completely independent process
  6. A “master” process coordinates these builds, meets the requirements and launches the VMs in parallel

The problem with the traditional Jenkins model with dedicated agents:

  • We install 3 dedicated Jenkins agents to parallelize the builds
  • Each VM has its own kernel and its own copy of the OS (resource cost)
  • These resources are consumed permanently, whether a build is in progress or not
  • VMs generally underperform containers (from IBM article)

The vision with Jenkins + Docker:

  • The system remains in a minimum idle state when nothing is happening
  • It scales automatically during active builds (each build gets its own container resources)
  • It reverts to minimal state after builds complete
  • Agents are ephemeral — they appear as needed and disappear when their work is done
Charge faible                    Charge haute
[Jenkins Master]                 [Jenkins Master]
                                 [Agent 1]  [Agent 2]  [Agent 3]
                                 (conteneurs lancés à la demande)

This is the model that we are going to build.


2.7 Demo: The Basics of Running Jenkins in a Container

Prerequisites: Docker installed and configured on your platform.

Step 1: Find the Jenkins image

docker search jenkins

Among the many results, the image to use is jenkins/jenkins — the central and complete image which is the official reference.

Step 2: Choose the right tag

  • jenkins/jenkins (without tag): weekly version, weekly update
  • jenkins/jenkins:lts: Long Term Support version — more stable, that’s what we want
# Télécharger l'image LTS
docker pull jenkins/jenkins:lts

You will see Docker downloading the different layers that make up this image.

Step 3: Check available images

docker image list

Step 4: Launch the Jenkins Container

docker run -d -p 2119:8080 --name jenkins-master jenkins/jenkins:lts
  • -d: detached mode (in background)
  • -p 2119:8080: maps container port 8080 to host port 2119
  • --name jenkins-master: name the container

Then navigate to http://localhost:2119 to access the Jenkins interface.

Initial Jenkins setup

  1. Recover the initial administrator password:
    docker exec jenkins-master cat /var/jenkins_home/secrets/initialAdminPassword
    
  2. Install default plugins
  3. Create the administrator account (cbehrens in the demo)
  4. Create a first test job

2.8 Maintain state outside container

One of the fundamental principles of working with containers is state isolation of the container.

How Docker manages layers

Containers are composed of layers:

  • OS
  • Application
  • Configuration
  • And so on…

These layers are read-only — they cannot be modified. Above these read-only layers is a writable file system layer (the TWL — Top Writeable Layer).

The difference between the image we downloaded (Jenkins LTS) and the container we are running is precisely this read/write layer, which now contains our plugins and our basic configuration (admin user, created jobs, etc.).

The most important approach: Pipelines and Jenkinsfiles

Before talking about volumes, there is an even more fundamental point:

Point #1 for keeping state outside the container: Use Pipelines and Jenkinsfiles.

In a classic freestyle build, the configuration and build steps are all stored in a config.xml file on the Jenkins file system (in the jobs directory). This state is related to the file system.

With Jenkinsfiles, you store your build definitions in version control and pull them dynamically as the first step in your build.

  • Build history, plugins and dependencies can be reconstructed
  • But your build definitions are your most important assets
  • Store them in Git, not the container

2.9 The Docker file system

Let’s imagine two containers created from the same jenkins/jenkins image:

  • Container A: default plugins installed, admin user = cbehrens
  • Container B: custom plugins installed, admin user = jsmith

Model 1: Complete independent copies

Conteneur A : [Couche OS] [App] [Config A] [TWL A]
Conteneur B : [Couche OS] [App] [Config B] [TWL B]

All layers are duplicated — very expensive.

Model 2: Dependent partial copies (reality)

Image partagée : [Couche OS] [App]  (lecture seule)
Conteneur A :                        [TWL A]  (écriture)
Conteneur B :                        [TWL B]  (écriture)

This is how Docker actually works.

Container − Image = Top Writeable Layer (TWL)

The TWL is the state of the container. To isolate the container state, simply take control of this TWL.


2.10 Understanding Copy-on-Write

To create the TWL, Docker uses a Copy-on-Write strategy:

Option 1 (not used): Keep full copies of the file system

Option 2 (the one used): Store only deltas (differences)

Here’s how it works:

  1. A Jenkins configuration file needs to be modified
  2. The storage driver searches for this file in the image layers
  3. It copies this file to the TWL
  4. The changes are then persisted in this copy

The key implication: When we take control of the TWL, we have a complete and faithful representation of the state of the container. No need to guess which directories to monitor.

The practical solution: Use volumes which do not use Copy-on-Write, but which exist directly on the host machine in the form of first citizens.


2.11 Demo: Mount a volume on your container

Important note: You cannot attach a new volume to an existing container (not simply). You need to create a new container.

Objective: Map the Jenkins home directory (/var/jenkins_home) directly to a host directory.

docker run -d \
  -p 2119:8080 \
  -v //c/docker/volumes/jenkins-master:/var/jenkins_home \
  --name jenkins-master \
  jenkins/jenkins:lts
  • -v //c/docker/volumes/jenkins-master:/var/jenkins_home: mount the volume
  • To the left of :: path on the host (Windows, Unix escaped style)
  • To the right of :: path in the container

On first startup, the host directory automatically populates with Jenkins files when the container starts.

# Vérifier que le répertoire est bien peuplé
ls //c/docker/volumes/jenkins-master/

You should see directories like jobs, plugins, secrets, users, etc.

What this means:

  • If you delete and recreate the container, your configuration is preserved
  • The volume can be backed up, replicated (RAID), or moved to a NAS
  • In a real world context, this volume would be on your NAS or file sharing system

2.12 Conclusion on Jenkins state

There’s something sophisticated going on with this volume on Windows: the Windows Docker service is talking to the Docker daemon on the Linux VM, which is talking to the Jenkins daemon in the container. All this translation is done automatically.

The two fundamental steps for managing Jenkins state:

  1. Most important: Switch to Pipelines / Jenkinsfiles — have your build definitions in version control. Having your builds in the volume is better than in the container, but it’s a distant second choice to version control.

  2. Take care of the volume: Backup, replicate, protect. Think of it like a database and apply the same availability and backup strategies.

Tip: Move your classic (freestyle) builds to pipelines. That’s one less thing to save in your Jenkins volume.


2.13 Section Summary

  • This is a good place to start. There is no reason not to use a container for Jenkins today.
  • We are moving towards a model where each application has its own user space — the container model makes a lot of sense for Jenkins, especially in view of the agent model we will be building.

3. Create a Jenkins build farm with Docker

Section duration: 57m 51s

3.1 Introduction

Now that the basics are established, we will take big steps towards the vision described previously: implementing ephemeral build agents — agents that appear and disappear as needed.

We will start with a single agent to fully understand the mechanism.


3.2 Demo: Provisioning Containerized Agents

Purpose: Enable the Jenkins instance to drive the Docker host to launch container instances on demand.

Prerequisites: Enable Remote Docker API

Docker host should expose its REST API on port 2375.

On Windows (Docker Desktop):

  1. Go to Docker Desktop Settings
  2. Check the box: “Expose daemon on tcp://localhost:2375 without TLS”

Known issue on Windows: Hyper-V reserves port 2375. If Docker Desktop starts fast enough, it gets it. Otherwise, Hyper-V takes it and nothing will work. The solution is to disable Hyper-V, release the port, reserve it, then re-enable Hyper-V and reboot. (See issue #3546 on GitHub for precise commands.)

Test Remote API

# Tester via curl ou navigateur
curl http://localhost:2375/version

If the API responds with JSON information about Docker, it’s good.

Configure Jenkins to use Docker as Cloud

In Jenkins:

  1. Manage JenkinsManage Nodes and Clouds
  2. Configure Clouds
  3. Add a new Docker type Cloud
  4. Configure Docker URI: tcp://host.docker.internal:2375
  5. Click on Test Connection to validate

Create an agent template

In Cloud Docker configuration:

  • Docker Image: jenkins/jenkins:lts
  • Remote Filing System Root: /var/jenkins_home
  • Labels: (leave blank for now)
  • Connect method: Attach Docker Container

Create and execute a first job

// Pipeline simple de test
pipeline {
    agent any
    stages {
        stage('Hello') {
            steps {
                echo 'Hello, World!'
            }
        }
    }
}

When running you will see Jenkins:

  1. Launch an agent container
  2. Run build on it
  3. Destroy container when finished

3.3 What happens with Cloud Agents

Here is the complete sequence when triggering a build:

  1. Jenkins resolves label (or “any” if no label required)
  2. Jenkins first searches among its attached nodes — none in our case
  3. Jenkins consults his Docker cloud for this label, finding the corresponding template
  4. Jenkins instance phones home to Docker host via REST calls
  5. Jenkins asks the Docker host to create a new container with the parameters specified in the template (by default, it pulls the image from Docker Hub)
  6. Jenkins provisions this container as an agent node, communicates with it via Jenkins Remoting technology
  7. Build runs — success or failure
  8. Jenkins destroys container — process is complete

Two major limitations at this stage

  1. Our agent has nothing installed. If we wanted to do a real build (not just Hello World), our simple image is not enough.

  2. The image must be in a public registry. For now, if we create our own image with the necessary dependencies, we need to push it to Docker Hub (public). We want to lock this down so as not to expose the security details of our builds.

Strategies for provisioning capable agents

Strategy 1: Find an existing image on Docker Hub that has the necessary prerequisites.


3.4 Understanding Docker Images and Trust

Don’t take images for granted. When it comes to Docker security, one fundamental principle applies:

Principle: Trust as little as possible, and be very careful who exactly you trust.

When we pull jenkins/jenkins, we trust the people and processes that create Jenkins — a trust already committed by deciding to use Jenkins. This is not extra confidence.

But if you download a Docker Hub image that claims to have the right prerequisites for a build tech… you’re trusting the integrity AND competence of that image’s creator.

Hanlon’s Razor:

“Never attribute to malice that which can be sufficiently explained by stupidity.”

Explicit attacks or malware on Docker images are fortunately rare. But the biggest risk is that more than necessary has been included in your image.

According to the Snyk 2019 Open-Source Vulnerabilities Report, many popular images contain known vulnerabilities.

Security principle: Prefer minimal base images.


3.5 Demo: Create a DotNetCore agent with a Dockerfile

Credit: Simplified version of a Dockerfile found on Stack Overflow, thanks to user Dennis Fazekas.

DotNetCore agent Dockerfile

FROM jenkins/jenkins:lts

USER root

# Mettre à jour la liste des paquets et les paquets installés
RUN apt-get update && apt-get upgrade -y

# Ajouter la clé publique Microsoft et le dépôt
RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.gpg

# Enregistrer le dépôt Microsoft avec APT
RUN sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-debian-stretch-prod stretch main" > /etc/apt/sources.list.d/dotnetdev.list'

# Installer le SDK .NET et l'ajouter au PATH automatiquement
RUN apt-get install dotnet-sdk-3.1 -y

# Vérifier l'installation
RUN dotnet --version

Explanation of steps:

  1. FROM jenkins/jenkins:lts: base image — agent inherits from Jenkins LTS
  2. USER root: we switch to root context for installations
  3. APT update: apt-get update && apt-get upgrade
  4. Microsoft key: download microsoft.asc, transform it with gpg --dearmor and place it in the trusted keys
  5. Microsoft repository: we create /etc/apt/sources.list.d/dotnetdev.list with the contents of the Microsoft repository (deb [arch=amd64] <url> stretch main)
  6. SDK installation: apt-get install dotnet-sdk-3.1 — automatically adds location to PATH
  7. Checking: dotnet --version shows the version in the Docker console

Image construction

# Se placer dans le répertoire contenant le Dockerfile
cd /path/to/dockerfile/directory

# Construire l'image (avec tag)
docker build -t agent-dnc:v1 - < agent-dnc.Dockerfile
  • -t agent-dnc:v1: name and tag the image
  • - < agent-dnc.Dockerfile: pipe the Dockerfile into the command

Image Test

# Lancer le conteneur
docker run -it agent-dnc:v1 bash

# Vérifier .NET
dotnet --version

To change .NET version: Simply modify the apt-get install dotnet-sdk-X.X instruction in the Dockerfile. You can push the result as a new tag, for example agent-dnc:v2.


3.6 Demo: Attach the DotNetCore agent to your Cloud

Prerequisites: The agent-dnc:v1 image is built and available (locally or in a registry).

Adding the template in Jenkins

  1. Manage JenkinsManage Nodes and CloudsConfigure Clouds
  2. In the Cloud Docker configuration, add a new agent template:
  • Docker Image: username/agent-dnc:v1 (or agent-dnc:v1 if local)
  • Labels: DOTNETCORE
  • Remote Filing System Root: /var/jenkins_home
  1. Save

Create a .NET Core project

A simple .NET Core console project (ConsoleApp1) with a Program.cs:

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, Jenkins-Docker!");
        }
    }
}

The project is versioned on GitHub.

Jenkinsfile for .NET Core build

pipeline {
    agent { label 'DOTNETCORE' }

    stages {
        stage('Checkout') {
            steps {
                git url: 'https://github.com/username/ConsoleApp1.git'
            }
        }
        stage('Build') {
            steps {
                sh 'dotnet build ConsoleApp1/ConsoleApp1.csproj'
            }
        }
    }

    post {
        always {
            archiveArtifacts artifacts: 'ConsoleApp1/**',
                             fingerprint: false,
                             allowEmptyArchive: true
        }
    }
}

Jenkinsfile key points:

  • agent { label 'DOTNETCORE' }: the build only runs on an agent with this label
  • If no DOTNETCORE agent is available, the build waits until one is available (very resilient)
  • Jenkins will automatically launch a container with the corresponding image

Running and Troubleshooting

Debugging tip: If the build does not start or the agent does not provision, check the Jenkins log (Manage Jenkins → System Log). This is where provisioning error messages are found, like “unable to find image in target registry”.


3.7 Conclusion on DotNetCore Agent

Strategy #2: Find a Dockerfile that matches the build configuration you need. Make sure you understand what it does.

Important points to remember:

  • Jenkinsfiles are durable — a build will wait patiently until a compatible agent is available
  • Debugging agent provisioning is done via Jenkins log
  • We have not mounted volumes for our agents — their workspace disappears with the container
  • This is the right way to do it: keeping a workspace potentially means having dependencies on the state of the build server (files missing in version control but present locally from a previous build)
  • If you want to keep the workspace, mount a volume for the agent

3.8 Demo: Create a Docker image meta-build

Purpose: Run Docker in a Docker Jenkins container to build and push images.

Required plugin

Install the Docker Pipeline plugin in Jenkins:

  • Manage JenkinsManage PluginsAvailable
  • Search for Docker Pipeline and install

Dockerfile of Docker-in-Docker agent

FROM jenkins/jenkins:lts

USER root

# Mettre à jour les paquets et installer les dépendances HTTPS
RUN apt-get update && apt-get install -y apt-transport-https ca-certificates

# Installer Docker en utilisant le script d'installation officiel
RUN curl -fsSL https://get.docker.com | sh

# Ajouter l'utilisateur Jenkins au groupe docker
RUN usermod -aG docker jenkins

# Au démarrage du conteneur : configurer le réseau et lancer le daemon Docker
CMD iptables -P FORWARD ACCEPT && dockerd

Explanation:

  • We start with jenkins/jenkins:lts as base layer
  • We install apt-transport-https and ca-certificates for HTTPS connections
  • Download and run the official Docker installation script from get.docker.com — you can check this script in a browser to see exactly what is being done
  • We add jenkins to the docker group so that it can access the Docker daemon
  • At startup: configure the network (legacy mode) and launch dockerd

Image construction

docker build -t agent-docker:v1 - < agent-docker.Dockerfile

Launching the container in privileged mode

docker run --privileged -d --name docker-build-agent agent-docker:v1

--privileged: Provides access to the kernel and other resources needed to launch the Docker daemon inside. Use with caution — this has important safety implications.

Checking from inside the container

# Récupérer l'ID du conteneur
docker ps

# Ouvrir un shell dans le conteneur en tant que jenkins
docker exec -it --user jenkins <container-id> bash

# Vérifier que Docker fonctionne et que jenkins a les permissions
docker --help
docker ps

If you see the Docker CLI help, it’s good: Docker is running in the container and user jenkins has permissions.

Jenkinsfile to build and push DotNetCore image

pipeline {
    agent { label 'DOCKER' }

    stages {
        stage('Build and Push') {
            steps {
                script {
                    docker.withRegistry('https://registry.hub.docker.com', 'dockerhubcreds') {
                        def image = docker.build("username/agent-dnc:v1",
                                                "-f agent-dnc.Dockerfile .")
                        image.push()
                    }
                }
            }
        }
    }
}

Explanation of docker.withRegistry:

  • First parameter: Registry URL
  • Second parameter: Jenkins credentials ID
  • The block executes all Docker operations in the context of this authenticated registry

When running in Jenkins, the Docker Pipeline plugin translates these Groovy commands into direct shell commands to the agent. You can see it in the output console: isUnix detects the OS and executes a sh command.


3.9 Meta-build considerations

What we just did is extremely powerful. Here are the implications:

Keeping images up to date

If you schedule this meta-build to run every night, your images will be kept up to date automatically with the LTS version of Jenkins, including all security fixes.

1. Construire la nouvelle image agent
       ↓
2. Lancer un conteneur avec cette nouvelle image
       ↓
3. Exécuter un build de test (ex: le dernier commit de votre code)
       ↓
4. Si succès → reconstruire l'image avec le tag "stable" ou "lts"
       ↓
5. Modifier vos templates d'agent pour pointer vers cette version stable
       ↓
Si échec → identifier le problème, modifier le Dockerfile, recommencer

This process can be fully automated (except perhaps modifying agent templates).

Jenkinsfile is 90% generic

By adding parameters for repository name and path, you can create a generic Docker build Jenkinsfile that can point to any repository following your convention.

Continuous integration for Docker images

With this system, you can set up a complete CI:

  • Pull source code → build → unit tests → launch test container → copy code → integration tests → push image – This is the holy grail of integration testing

3.10 Understanding container connection methods

Jenkins can connect to its agents in three different ways:

MethodDescription
Attach ContainerUses the Docker host’s ability to pass commands and data between the master and the agent image
JNLPJava Network Launch Protocol (also called Web Start in Java)
SSHSecure Shell

Comparison of prerequisites

PrerequisitesAttach ContainerJNLPSSH
Java installed
Master accessible network
Agent initiates connection
Traffic encryption

The key difference: JNLP and SSH require the master to be accessible over the network by the agent, while Attach Container does not require it.

When to use SSH?

The only case justifying not using Attach Container (the default method):

  • You are concerned about intercepting traffic between Jenkins master and container
  • We are operating without TLS on port 2375: if something can listen to this port in the clear, a man-in-the-middle attacker could read the data

JNLP communication is binary but unencrypted — any security in there would just be security in obscurity.

Recommendation: For secure environments or if credentials are passed to agents, consider SSH.


3.11 Working with private registers

It will not be practical or possible for all of your Docker images to be publicly accessible. Whether you use a private registry in Docker Hub or a completely private registry behind your firewall, you will need to manage credentials.

What’s special about Docker Hub: It is the default registry. That’s all that sets it apart from other registers. Any other registry supporting Docker Registry protocols should work the same.

Two issues to resolve

  1. Pushing to a private repository via a build — mostly already solved with docker.withRegistry
  2. Draw a private image for the agent — this is the most critical point: this is how you will work in practice with your agent images

Security note

Credentials pass from the Jenkins master to the Docker agent via the unencrypted channel on port 2375. This is exactly the type of traffic you might want to encrypt if your build network is open. That’s a good reason to switch to SSH for attach in this case.


3.12 Demo: Authentication to a custom registry

Goal: Make our agent-dnc repository private on Docker Hub and make sure everything works.

Step 1: Make the repository private on Docker Hub

  1. Go to Docker Hub → username/agent-dnc
  2. Settings → make private
  3. Enter the repository name to confirm

Step 2: Edit the Jenkinsfile to use the explicit Docker Hub URL

pipeline {
    agent { label 'DOCKER' }

    stages {
        stage('Build and Push') {
            steps {
                script {
                    // Spécifier l'URL complète du registre + credentials
                    docker.withRegistry('https://registry.hub.docker.com', 'dockerhubcreds') {
                        def image = docker.build("username/agent-dnc:v1",
                                                "-f agent-dnc.Dockerfile .")
                        image.push()
                    }
                }
            }
        }
    }
}

Step 3: Configure the agent template for authentication

In the Cloud Docker configuration, in the DotNetCore agent template:

  • Add the Registry Credentials pointing to the created Jenkins credential (dockerhubcreds)

Console warning to note

In the output console, you will see:

WARNING! Your password will be stored unencrypted in /workspace/Build/...
Configure a credential helper to remove this warning.

This workspace has not existed for long, but if the image is compromised in any way, these credentials would be at risk. It’s worth exploring credential helpers for this reason.

For a custom private register

Simply replace the Docker Hub registry URL with your private registry URL, ensuring you have the appropriate connectivity and credentials.


3.13 Install dependencies dynamically

There is one last approach to explore — mainly to discourage you: installing dependencies directly into the Jenkinsfile.

The concept

Use a very generic agent (like jenkins/jenkins:lts) and install the tools in the Jenkinsfile:

pipeline {
    agent { label 'DOCKER' }  // agent générique
    stages {
        stage('Setup') {
            steps {
                sh 'curl -fsSL https://install-something.com | bash'
                sh 'apt-get install -y build-essential nodejs'
            }
        }
    }
}

Why is it not working

User jenkins in the LTS image does not have permissions to perform system installations. If you run this code, you will quickly get a Permission denied error.

Jenkins is not supposed to download and install things on the box it’s running on. This is not how Jenkins is supposed to work.

Alternatives (with their disadvantages)

Alternative 1: Store tools in version control

  • Undesirable — tools are often binaries that have no place in Git
  • Tools will quickly diverge unless you commit to keeping updates in version control
  • This works because user jenkins can do a git clone very well

Alternative 2 (recommended): Create specialized agent images with Dockerfile

  • This is the correct approach described in previous sections
  • All installations are done in the Dockerfile where root is available

Conclusion: The real solution is to create proper Dockerfile images with the right prerequisites. Dynamic installation in Jenkinsfiles is not the right approach.


3.14 Working with ephemeral agents

With traditional dedicated Jenkins agents, the persistent workspace is often used to diagnose build failures. With ephemeral agents, everything disappears with the container.

Why this is a good thing

  • Verification builds for pull requests: we don’t care about the results
  • Avoid dependencies on build server state (“ghost” files that should be in version control)
  • Clean and repeatable builds every time

Manage ephemeral build releases

In practice you want to do something with the outputs. The usual options:

  • Push results into an artifact repository (Artifactory, Nexus)
  • Send them to a deployment server en route to staging/production

For simpler cases, Jenkins artifacts allow you to retrieve the results directly in the Jenkins interface.


3.15 Demo: Working with artifacts on ephemeral agents

The archiveArtifacts step in the Jenkinsfile

pipeline {
    agent { label 'DOTNETCORE' }
    stages {
        stage('Build') {
            steps {
                sh 'dotnet build ConsoleApp1/ConsoleApp1.csproj'
            }
        }
    }
    post {
        always {
            // Archiver tous les fichiers du projet
            // Pas de suppression si le build échoue (on veut les fichiers pour déboguer)
            archiveArtifacts artifacts: 'ConsoleApp1/**',
                             fingerprint: false,
                             allowEmptyArchive: true
        }
    }
}

archiveArtifacts options:

  • artifacts: glob pattern to select files to archive
  • fingerprint: calculate a SHA1 hash to track artifact usage
  • onlyIfSuccessful: if true, only archives if the build succeeds (do not use for debugging)
  • allowEmptyArchive: if true, does not fail the build if no file is found

Tip: For debugging, check in ConsoleApp1/** (the whole project) rather than just the build output directory. This gives the widest net to find problems.

Access artifacts

In the Jenkins interface:

  1. Go to the main build page
  2. You will see a list of archived files
  3. Click on a file to download and inspect it

Practical case: debug a broken build

If someone enters an error in Program.cs, the build fails. Although the error is visible in the output console, sometimes the problem is subtle (missing file rather than syntax error). Artifacts allow you to:

  • Navigate build files
  • Download and inspect specific files
  • Compare with previous versions

3.16 More specific Jenkins images

Our current setup uses jenkins/jenkins:lts as the image for agents — the full Jenkins server image with web UI and everything else. This is not optimal.

Why the full picture is suboptimal for agents

  1. Security principle: We want the lightest Docker image possible. The Jenkins server web UI is not needed on an agent. If there was a vulnerability in the UI code, a compromised agent would be a waste.

  2. Performance: A lighter image potentially means fewer running services, less network traffic, and certainly a smaller disk footprint.

Lighter images

Images to consider:

  • jenkins/agent: base image for agents
  • jenkins/inbound-agent: the most common default agent

“Inbound” means that the Jenkins master initiates the connection to the agent (rather than the other way around). This is normal mode.

There are several variations in the official Jenkins registry — take the time to browse the catalog.

Practical recommendation

Get everything working with jenkins/jenkins:lts first, then experiment with lighter images like jenkins/inbound-agent.

This technology is complex and thin images may exhibit differences in behavior. Starting with the full image ensures that any issues you encounter come from your configuration and not differences between images.


3.17 Section Summary

In this section, we’ve covered a lot of ground:

  1. Simplest agent: built directly from jenkins/jenkins:lts, executed a Hello World build
  2. DotNetCore Agent: Dockerfile based on Jenkins image with .NET SDK installed
  3. Docker-in-Docker Agent: Docker running in a Docker Jenkins container to build and push images
  4. Private Connections: Connecting to private GitHub repositories and private Docker Hub
  5. Artifacts: preservation of the state of ephemeral agents via archiving

You now have the tools to create your own agents with dependencies specific to your needs.


4. Working with Multi Architecture Containers in Jenkins

Section duration: 24m 18s

4.1 Understanding multi-architecture

So far, we’ve used Jenkins and Docker to do typical build work: pulling code, compiling, and following a typical software lifecycle. Now we’ll focus on issues related to building Docker images themselves for multiple architectures.

The problem

If your company produces Docker images as a deliverable (or for internal use), and you want to target multiple OS or processor platforms, you find yourself building separate images:

  • ourenterprise/ourproduct-windows
  • ourenterprise/ourproduct-linux

And if you target different processor architectures, the number of frames multiplies further.

Concrete example: BusyBox

BusyBox (the “Swiss army knife of embedded Linux”) runs on many POSIX OS and CPU architectures. To support this broad base of installation options, BusyBox uses what is called a multi-arch repository.

The challenge with .NET Core

Our .NET Core application can run on many platforms and architectures (Linux x64, Linux ARM, Windows x64, macOS, etc.). If we ship via Docker images, we will need one image per target.

To some degree, this is inevitable — you can’t have a single Dockerfile building a cross-platform image. However, there are smart ways to handle this.


4.2 Demo: Cross-architecture negotiation in Docker

Inspect the architecture of an image

# Inspecter le format/plateforme d'une image
docker image inspect jenkins/jenkins:lts --format '{{.Os}}/{{.Architecture}}'
# Résultat : linux/amd64

Attempt to pull an incompatible image

# Essayer de tirer une image Windows sur une plateforme Linux
docker pull docker/ucp-agent-win:latest

Result:

Unable to find image 'docker/ucp-agent-win:latest' locally
no matching manifest for linux/amd64 in the manifest list entries

Docker automatically detects your platform (linux/amd64) and refuses to pull an image that is not compatible (windows/amd64). The message is clear: it’s in the image name (ucp-agent-win), and your current Docker platform is Linux.

Switch to Windows Containers (on Windows)

Via Docker Desktop (icon in notification bar) → Switch to Windows containers

After failover:

# Retirer l'image .NET Core Runtime (téléchargera la version Windows)
docker pull mcr.microsoft.com/dotnet/runtime:5.0

# Inspecter : maintenant c'est windows/amd64
docker image inspect mcr.microsoft.com/dotnet/runtime:5.0 --format '{{.Os}}/{{.Architecture}}'
# Résultat : windows/amd64

What’s happening: By switching to Windows containers, Docker Desktop no longer goes through the intermediate Linux VM — it communicates directly with the Windows service. Docker automatically downloads the image corresponding to your current platform.


4.3 How BuildX builds for unavailable platforms

We don’t have an ARM Raspberry Pi to build ARM images. So how do we do it?

QEMU: CPU emulation

The answer is in Docker’s buildx stack: QEMU processor emulators.

QEMU (Quick Emulator) is a hardware virtualizer/emulator. When you run QEMU, you can use the emulation layer to simulate the instruction set and behavior of a processor that you don’t actually have, allowing you to run a version of Linux compiled for that architecture.

Analogy: MAME (Multi Arcade Machine Emulator)

To better understand emulation, consider MAME, the arcade emulator:

  • In the 80s, each arcade machine had its own specific hardware
  • Pac-Man ran on a Zilog Z80 processor
  • Battlezone was running on a MOS 6502
  • These two processors had entirely different architectures

MAME creates a hardware emulation layer for each of these processors. With this, MAME can load the games’ original ROMs (the real 0s and 1s of the original software code) and run them as if the original hardware was present. The game behaves almost exactly like it does on the real machine — so much so that a recent record submission to Donkey Kong was only revealed to be fraudulent by examining the behavior of video scan lines, which operate slightly differently under MAME than on the original hardware.

Applying to Docker buildx

Similarly, buildx with QEMU can emulate an ARM processor on your AMD64 machine, allowing you to build Docker images for ARM without having physical ARM hardware.


4.4 Demo: Building multi-architecture Docker images

Check available builders

docker buildx ls

Typical result:

NAME/NODE       DRIVER/ENDPOINT   STATUS    PLATFORMS
default *       docker
  default       default           running   linux/amd64, linux/386
mybuilder       docker-container
  mybuilder0    default           running   linux/amd64, linux/386, linux/arm/v7, linux/arm/v6

Note: amd64 and x86_64 are two names for the same thing. Like Frisbee and flying disc — one is a brand name.

Physical builders with buildx

You can also configure real physical devices as builders:

# Ajouter un builder distant (un vrai appareil ARM, PowerPC, etc.)
docker buildx create --name mybuilder --append ssh://user@arm-device

This may be better or worse than emulation depending on your device.

Dockerfile for a multi-arch image (dncgit.Dockerfile)

# Image de base multi-plateforme du SDK .NET
FROM mcr.microsoft.com/dotnet/sdk:5.0

# Mettre à jour et installer Git
RUN apt-get update && apt-get install git -y

This Dockerfile is simple: we start with the .NET SDK (which supports several architectures) and we install Git. This is a development environment for PC and ARM device developers.

Build and push for multiple architectures

# Étape 1 : Créer un espace de build dédié
docker buildx create --name cbbspace

# Étape 2 : Utiliser ce builder
docker buildx use cbbspace

# Étape 3 : Construire pour amd64 ET arm/v7 et pousser
docker buildx build \
  --platform linux/amd64,linux/arm/v7 \
  -t username/dncgit:latest \
  --push \
  -f dncgit.Dockerfile \
  .
  • --platform linux/amd64,linux/arm/v7: build for two architectures simultaneously
  • --push: push directly to the registry (the --push is necessary because buildx does not store multi-arch images locally)
  • The process compiles both images in parallel and creates a multi-arch manifest on Docker Hub

After running, check on Docker Hub — you will see the image with manifests for linux/amd64 and linux/arm/v7.


4.5 Build with BuildKit

When launching buildx for the first time, you will see the message: “build with BuildKit”.

BuildKit is a new Docker image build engine. The key differences:

AppearanceClassic engineBuildKit
TreatmentSerialParallel (as much as possible)
CacheBasicBased on Git-like heuristics
PerformanceStandardSignificantly faster

The plan: the X in buildx will disappear and BuildKit will become Docker’s base engine for creating images. BuildKit has been around for a few years and is quite stable.

Note: Although BuildKit is stable, the author does not yet recommend running buildx commands via Jenkins in production. That said, using buildx build outputs is perfectly acceptable.


4.6 Demo: Building multi-architecture images in Jenkins-Docker

Purpose: Manually run multi-arch build steps in our Docker-in-Docker Jenkins agent, in preparation for automation via Jenkinsfile.

Connect to Docker-in-Docker agent

# Lancer l'agent Docker en mode privilégié
docker run --privileged -d --name docker-build-agent agent-docker:v1

# Ouvrir un shell dans le conteneur
docker exec -it docker-build-agent bash

Recreate the multi-arch build sequence

In the container:

# 1. Créer le builder
docker buildx create --name cbbspace

# 2. Utiliser ce builder
docker buildx use cbbspace

Get the Dockerfile from the host (from the host command line):

# Depuis l'hôte : copier le Dockerfile dans le conteneur
docker cp dncgit.Dockerfile docker-build-agent:/workspace/dncgit.Dockerfile

Return to container:

# 3. S'authentifier sur Docker Hub
docker login -u username -p password
# (Dans Jenkins, on utilisera les credentials Jenkins à la place)

# 4. Le grand build multi-arch : construire pour AMD64 et ARM7, pousser
docker buildx build \
  --platform linux/amd64,linux/arm/v7 \
  -t username/dncgit:latest \
  --push \
  -f /workspace/dncgit.Dockerfile \
  .

After a few moments, the image is built and pushed. Check on Docker Hub — you will see the image with its multi-arch tags.


4.7 Integrate this into Jenkins

Summary of steps to automate:

  1. Create a custom build space (cbbspace) → already in the script
  2. Tell Docker to use this builder → already in the script
  3. Copy the Dockerfile into the containeroffered for free by Jenkins job provisioning (SCM checkout)
  4. Create a credential set for Docker Hub authentication → in Jenkins via docker.withRegistry
  5. Run multi-arch build → in Jenkinsfile
  6. Push images → included in buildx --push

4.8 Demo: Getting this to work in Jenkins

Jenkinsfile for multi-arch build (build-simple-dnc.jenkinsfile)

pipeline {
    agent { label 'DOCKER' }

    stages {
        stage('Build and Push Multi-arch') {
            steps {
                script {
                    docker.withRegistry('https://registry.hub.docker.com', 'dockerhubcreds') {
                        // Créer le builder (|| true pour ne pas échouer si déjà existant)
                        sh 'docker buildx create --name cbbspace || true'

                        // Utiliser le builder
                        sh 'docker buildx use cbbspace'

                        // Build multi-arch et push en une seule commande
                        sh '''docker buildx build \
                            --platform linux/amd64,linux/arm/v7 \
                            -t username/dncgit:latest \
                            -t username/dncgit:jenkinsfile \
                            --push \
                            -f dncgit.Dockerfile \
                            .'''
                    }
                }
            }
        }
    }
}

Validation tip: We add the jenkinsfile tag to ensure that the image comes from this Jenkins build. If we go to Docker Hub and find the tag jenkinsfile, we know that it comes from Jenkins.

Configure the job in Jenkins

  1. New Item → name Build-SimpleDNC → type Pipeline
  2. Scroll to Pipeline → select Pipeline from SCM
  3. SCM: Git
  4. Repository URL: URL of your GitHub repository
  5. Script Path: build-simple-dnc.jenkinsfile
  6. Save and trigger the build

In the output console, you will see the build process running — parallel builds are running inside your Jenkins Docker container. Once finished, check Docker Hub to confirm the presence of the jenkinsfile tag.


4.9 Conclusion and outlook

For this simple product image build, we limited ourselves to simple elements. In a real-world context, you will likely combine:

  • Multi-step Jenkinsfiles
  • Multi-stage Dockerfiles

Example: .NET Core application with multi-step Dockerfile

# Étape 1 : Build
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src

# Copier et restaurer les dépendances
COPY ["ConsoleApp1/ConsoleApp1.csproj", "ConsoleApp1/"]
RUN dotnet restore "ConsoleApp1/ConsoleApp1.csproj"

# Copier le code source et construire
COPY ConsoleApp1/ ConsoleApp1/
WORKDIR /src/ConsoleApp1
RUN dotnet build "ConsoleApp1.csproj" -c Release -o /app/build

# Étape 2 : Publish
FROM build AS publish
RUN dotnet publish "ConsoleApp1.csproj" -c Release -o /app/publish

# Étape 3 : Image finale minimale (seulement le Runtime, pas le SDK)
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ConsoleApp1.dll"]

Advantages:

  • Final image only contains Runtime (not full SDK) → much smaller image
  • Source code is pulled via Git into build image
  • Microsoft provides samples for .NET Core and Docker on GitHub

The Jenkins + Docker hybrid approach

For unit testing and static analysis (which have no native support in Dockerfiles), the optimal approach is:

Pipeline Jenkins (sur image agent appropriée)
  ↓
1. Checkout du code source
2. Tests unitaires
3. Analyse statique (SonarQube, etc.)
4. Build de l'artefact
5. Placer la sortie dans un répertoire fiable
  ↓
Dockerfile
  COPY --from=build-output ./output /app
  (ou comme première étape du Dockerfile)

It’s the best of both worlds:

  • All the power and maintainability of a Jenkins pipeline for testing and quality
  • The efficiency of Docker for packaging and delivery

4.10 Section Summary

  1. Understanding the multi-architecture challenge
  2. The traditional approach vs. how BuildX solves this problem with QEMU
  3. Demonstrating a manual multi-arch build from the command line
  4. Docker-in-Docker with multi-arch builds experimental
  5. Automation in Jenkins via a Jenkinsfile

5. Maintain your build farm

Section duration: 13m 45s

5.1 Introduction

This course has largely been a snapshot in time. But as a DevOps professional, you will work with these technologies over time. This section covers:

  • How upgrades work in this container world
  • The stability and security of everything we have done

5.2 Upgrade Jenkins

The classic native model problem

If you’ve worked with the native Jenkins installation model, you know that it’s very easy to find yourself six or seven versions behind the current version — and then you face the risk of an upgrade breaking your build server.

This creates a downward spiral:

  1. You are not upgrading often enough
  2. Each individual upgrade is more painful
  3. This requires postponing subsequent upgrades
  4. Making them even more painful, etc.

The fundamental DevOps principle

“If something hurts, do it often.”

This is the founding principle of DevOps. When a procedure goes from ad hoc to routinized and standardized, it becomes predictable, optimizable — faster, cheaper, better.

The good news with containers

The good news is that the way we run Jenkins gives us a lot of this for free.

Here is the mechanism:

  1. We work with jenkins/jenkins:lts
  2. At some point, the LTS version will change — a new version will be tagged lts
  3. The base of your Jenkins image will change
  4. This means your Jenkins instance will be different than it was before

The good news: It’s designed to work like this in Jenkins. When you restart your container, Jenkins automatically handles data migration.

Automatic upgrade process

Nouveau tag LTS publié sur Docker Hub
       ↓
Vous redémarrez votre conteneur Jenkins
       ↓
Docker télécharge automatiquement la nouvelle image
       ↓
Jenkins démarre avec la nouvelle version
       ↓
Jenkins effectue les migrations nécessaires automatiquement
       ↓
Si une notification apparaît pour formater les données,
cliquer le bouton de correction suffit

5.3 Demo: Maintain and upgrade Jenkins

Check current version

Manage Jenkins → About → Jenkins core

Version at time of registration: 2.378.1 (LTS)

What happened during LTS update

While writing this course, Jenkins LTS was at version 2.204.6 at the start. Version 2.222 was released as a new LTS. What happened:

  • Some UI changes, font differences, and other modifications
  • When restarting the Jenkins container, everything upgraded automatically, taking just a little longer than usual to start
  • At login, UI changes were visible, confirming the new version
  • A notification was received that some data had changed format — one click on the correction button was enough

Consult the LTS changelog

https://www.jenkins.io/changelog-stable/

This changelog lists the changes for each LTS release in detail, with detailed notes on what may be broken and what is fixed.


5.4 Upgrade plugins

What is automatic

With the Jenkins upgrade, you also get a free upgrade to the standard plugins that are part of the LTS.

What is not

If you have manually upgraded plugins from the version that made up the previous LTS, the Jenkins upgrade engine will detect them and leave them alone.

Option 1: Manual upgrade

Find and manually install the version of the plugin compatible with the new LTS. This is tedious because the compatible version is not necessarily the latest version of the plugin.

Option 2: PLUGINS_FORCE_UPGRADE environment variable

docker run -d \
  -e PLUGINS_FORCE_UPGRADE=true \
  -p 2119:8080 \
  -v //c/docker/volumes/jenkins-master:/var/jenkins_home \
  --name jenkins-master \
  jenkins/jenkins:lts

Additional benefit: The manual upgrade flag is removed, and future LTS updates will properly update the plugins.

Summary: If you need to upgrade a plugin mid-LTS cycle, use this approach. Otherwise, wait for it to happen automatically with the LTS cycle.

Note: This approach does nothing for plugins you have installed that are not part of the LTS image — including enterprise plugins developed internally. For these, you will have to do the updates manually.

Extracted principle

Restart your Jenkins container when you are ready to upgrade, and not before.

In general, we want to avoid restarting containers — this is a Docker principle. But this is one of the cases where restarting your Jenkins container has an absolutely compelling use case.

Note: Docker Live Restore is a different thing. You will actually need to restart this container to upgrade it.


5.5 Jenkins and Infrastructure as Code

Approach with Groovy scripts

One way to manage Jenkins configuration like Infrastructure as Code is to use Groovy scripts to drive the configuration. This is a powerful but more advanced approach (see the “Automating Jenkins with Groovy” course).

JCasC: Jenkins Configuration as Code

The preferred method here is the JCasC plugin (Jenkins Configuration as Code). Instead of Groovy scripts, JCasC uses YAML files for configuration.

JCasC YAML file example:

credentials:
  system:
    domainCredentials:
      - credentials:
          - usernamePassword:
              scope: GLOBAL
              id: sonar-credentials
              username: admin
              password: ${SONAR_PASSWORD}

unclassified:
  sonarGlobalConfiguration:
    installations:
      - name: "SonarQube"
        serverUrl: "http://localhost:9000"
        credentialsId: "sonar-credentials"

This example creates domain credentials and uses them to connect to a local SonarQube instance.

SonarQube is a static analysis tool that examines your code and makes recommendations to improve quality, security, and more.

If everything is configured correctly (environment variable and YAML file in the right place), this configuration is automatically applied when starting a Jenkins instance.

Compare JCasC with volumes

Classic volume approach:

  1. Configure a Jenkins instance (modifies config.xml and other files in the volume)
  2. For new instances (master or agent), base on this volume — create a copy and make modifications
  3. Store volume contents in version control

JCasC approach:

  1. Write configuration YAML files
  2. Store these YAML files in version control
  3. Configuration is applied automatically at startup

What JCasC provides:

  • Centralization and normalization — instead of storing instance configuration data (binary, verbose XML), you store a clean, readable declaration
  • Configuration becomes declarative and versionable properly
  • Allows you to easily create identical Jenkins instances
  • Reproducible and auditable infrastructure

5.6 Important points to consider

Mount Docker socket

# Cette opération est commune dans les configurations Docker-in-Docker
-v /var/run/docker.sock:/var/run/docker.sock

Warning: According to a post on Y Combinator: “Please, for the love of all that is holy, do not ever do this. Giving access to the host’s Docker socket is equivalent to giving a container unrestricted root access to your host.”

And he’s right. Giving access to the host’s Docker socket is equivalent to giving full root access to your host.

However: To make Docker-in-Docker work, there is no choice but to do this.

The nuance: The post mainly talks about doing this with containers taken at random from the Internet, without knowing their origin or their contents. In this case, don’t do it — don’t use these containers at all.

For our case: we can trust the Jenkins LTS image not to be compromised. And as long as we’re working with minimal Dockerfiles, this is a practice that, while not ideal, is probably acceptable — provided we remember the implications for any Jenkins cloud agents that are going to use Docker.

Minimal plugin profile

Pure Jenkins principle (not specific to Docker): All your Jenkins instances should run with a minimal plugins profile.

Each plugin you run in Jenkins has:

  • Its own security attack surface
  • Its own performance impact under the right circumstances
  • Its own stability issues and flaws associated with its development process

If you are not using a plugin, it should not be installed. And if you install it, keep it updated.

Summary of security considerations

RiskMitigation
Docker socket mountedUse only trusted images (Jenkins LTS), minimal Dockerfiles
Unencrypted traffic (port 2375)Consider TLS or SSH for sensitive environments
Credentials in plain text in the workspaceUse Docker credential helpers
Plugins with vulnerabilitiesMinimal profile + regular updates
Images with too much attack surfacePrefer minimal base images

5.7 Course Summary

Here is what we accomplished in this course:

Section 2: Basics

  • Mastered the fundamental difference VM vs container (the kernel)
  • Understands Windows hybrid architecture (Hyper-V + Linux VM + Container)
  • Launched Jenkins in a container
  • Managed Jenkins state with volumes

Section 3: Build Farm

  • Provisioned ephemeral agents via Docker Remote API
  • Created specialized agents with custom Dockerfiles
  • Created meta-builds (Docker-in-Docker)
  • Managed private registers and credentials
  • Preserved results via Jenkins artifacts

Section 4: Multi-architecture

  • Understood the multi-arch challenge
  • Used BuildX and QEMU to build for unavailable platforms
  • Automated multi-arch build in Jenkins

Section 5: Maintenance

  • Automatic upgrade process with LTS tag
  • Managing plugins with PLUGINS_FORCE_UPGRADE
  • JCasC for configuration as Infrastructure as Code
  • Security considerations specific to Jenkins-Docker

6. Appendix: Command Reference

Essential Docker Commands

# Rechercher une image
docker search jenkins

# Télécharger une image LTS
docker pull jenkins/jenkins:lts

# Lister les images locales
docker image list

# Lancer Jenkins sans volume
docker run -d -p 2119:8080 --name jenkins-master jenkins/jenkins:lts

# Lancer Jenkins avec volume
docker run -d \
  -p 2119:8080 \
  -v //c/docker/volumes/jenkins-master:/var/jenkins_home \
  --name jenkins-master \
  jenkins/jenkins:lts

# Lancer Jenkins avec mise à niveau forcée des plugins
docker run -d \
  -e PLUGINS_FORCE_UPGRADE=true \
  -p 2119:8080 \
  -v //c/docker/volumes/jenkins-master:/var/jenkins_home \
  --name jenkins-master \
  jenkins/jenkins:lts

# Lancer un agent Docker-in-Docker en mode privilégié
docker run --privileged -d --name docker-build-agent agent-docker:v1

# Ouvrir un shell dans un conteneur
docker exec -it jenkins-master bash
docker exec -it --user jenkins docker-build-agent bash

# Copier un fichier vers un conteneur
docker cp dncgit.Dockerfile docker-build-agent:/workspace/dncgit.Dockerfile

# Lister les conteneurs en cours
docker ps

# Arrêter et supprimer un conteneur
docker stop jenkins-master && docker rm jenkins-master

# Récupérer le mot de passe admin initial Jenkins
docker exec jenkins-master cat /var/jenkins_home/secrets/initialAdminPassword

Docker BuildX Commands

# Lister les builders disponibles et leurs plateformes
docker buildx ls

# Créer un nouveau builder
docker buildx create --name cbbspace

# Utiliser un builder spécifique
docker buildx use cbbspace

# Construire pour une seule architecture
docker buildx build -t username/image:tag -f Dockerfile .

# Construire pour plusieurs architectures et pousser
docker buildx build \
  --platform linux/amd64,linux/arm/v7 \
  -t username/image:latest \
  -t username/image:v1.0 \
  --push \
  -f image.Dockerfile \
  .

# Ajouter un builder distant (appareil physique)
docker buildx create --name mybuilder --append ssh://user@arm-device

Image Build Commands

# Construire à partir d'un Dockerfile
docker build -t agent-dnc:v1 - < agent-dnc.Dockerfile

# Construire et tagger
docker build -t username/agent-dnc:v1 -f Dockerfile .

# Pousser vers Docker Hub
docker push username/agent-dnc:v1

# S'authentifier sur Docker Hub
docker login -u username

Inspection commands

# Vérifier la plateforme d'une image
docker image inspect jenkins/jenkins:lts --format '{{.Os}}/{{.Architecture}}'

# Inspecter les détails d'un conteneur
docker inspect jenkins-master

# Voir les logs d'un conteneur
docker logs jenkins-master

# Tester l'API Remote Docker
curl http://localhost:2375/version

7. Appendix: Complete Code Examples

DotNetCore agent Dockerfile (agent-dnc.Dockerfile)

FROM jenkins/jenkins:lts

USER root

# Mettre à jour la liste des paquets et les paquets installés
RUN apt-get update && apt-get upgrade -y

# Ajouter la clé publique Microsoft
RUN curl https://packages.microsoft.com/keys/microsoft.asc \
    | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.gpg

# Enregistrer le dépôt Microsoft avec APT (Debian Stretch)
RUN sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-debian-stretch-prod stretch main" \
    > /etc/apt/sources.list.d/dotnetdev.list'

# Installer le SDK .NET Core 3.1
RUN apt-get update && apt-get install dotnet-sdk-3.1 -y

# Vérifier l'installation (affiche la version dans la console Docker)
RUN dotnet --version

Dockerfile agent Docker-in-Docker (agent-docker.Dockerfile)

FROM jenkins/jenkins:lts

USER root

# Mettre à jour et installer les dépendances nécessaires pour HTTPS
RUN apt-get update && apt-get install -y \
    apt-transport-https \
    ca-certificates

# Installer Docker en utilisant le script officiel
RUN curl -fsSL https://get.docker.com | sh

# Ajouter l'utilisateur Jenkins au groupe docker
RUN usermod -aG docker jenkins

# Au démarrage : configurer le réseau et lancer le daemon Docker
CMD iptables -P FORWARD ACCEPT && dockerd

Dockerfile multi-arch developer image (dncgit.Dockerfile)

# Image SDK .NET multi-plateforme comme base
FROM mcr.microsoft.com/dotnet/sdk:5.0

# Installer Git
RUN apt-get update && apt-get install git -y

Multi-stage Dockerfile .NET Core

# === Étape 1 : Build ===
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src

# Copier et restaurer les dépendances (couche mise en cache)
COPY ["ConsoleApp1/ConsoleApp1.csproj", "ConsoleApp1/"]
RUN dotnet restore "ConsoleApp1/ConsoleApp1.csproj"

# Copier le reste du code et construire
COPY ConsoleApp1/ ConsoleApp1/
WORKDIR /src/ConsoleApp1
RUN dotnet build "ConsoleApp1.csproj" -c Release -o /app/build

# === Étape 2 : Publish ===
FROM build AS publish
RUN dotnet publish "ConsoleApp1.csproj" -c Release -o /app/publish

# === Étape 3 : Image finale (Runtime uniquement) ===
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ConsoleApp1.dll"]

Jenkinsfile build DotNetCore with artifacts

pipeline {
    agent { label 'DOTNETCORE' }

    stages {
        stage('Checkout') {
            steps {
                git url: 'https://github.com/username/ConsoleApp1.git',
                    branch: 'main'
            }
        }

        stage('Build') {
            steps {
                sh 'dotnet build ConsoleApp1/ConsoleApp1.csproj -c Release'
            }
        }

        stage('Test') {
            steps {
                sh 'dotnet test ConsoleApp1.Tests/ConsoleApp1.Tests.csproj'
            }
        }
    }

    post {
        always {
            // Archiver les artefacts (même en cas d'échec, pour le débogage)
            archiveArtifacts artifacts: 'ConsoleApp1/**',
                             fingerprint: false,
                             allowEmptyArchive: true
        }
    }
}

Jenkinsfile meta-build (build and push a Docker image)

pipeline {
    agent { label 'DOCKER' }

    stages {
        stage('Build and Push Docker Image') {
            steps {
                script {
                    docker.withRegistry('https://registry.hub.docker.com', 'dockerhubcreds') {
                        def image = docker.build(
                            "username/agent-dnc:v1",
                            "-f agent-dnc.Dockerfile ."
                        )
                        image.push()
                        image.push('latest')  // Pousser aussi avec le tag 'latest'
                    }
                }
            }
        }
    }
}

Jenkinsfile multi-architecture build (build-simple-dnc.jenkinsfile)

pipeline {
    agent { label 'DOCKER' }

    stages {
        stage('Build Multi-arch and Push') {
            steps {
                script {
                    docker.withRegistry('https://registry.hub.docker.com', 'dockerhubcreds') {
                        // Créer l'espace de build (ignore l'erreur si déjà existant)
                        sh 'docker buildx create --name cbbspace || true'

                        // Utiliser ce builder
                        sh 'docker buildx use cbbspace'

                        // Construire pour AMD64 et ARM v7, pousser en une commande
                        sh '''docker buildx build \
                            --platform linux/amd64,linux/arm/v7 \
                            -t username/dncgit:latest \
                            -t username/dncgit:jenkinsfile \
                            --push \
                            -f dncgit.Dockerfile \
                            .'''
                    }
                }
            }
        }
    }
}

JCasC YAML example (jenkins.yaml)

# Configuration JCasC pour Jenkins
credentials:
  system:
    domainCredentials:
      - credentials:
          - usernamePassword:
              scope: GLOBAL
              id: "sonar-credentials"
              username: "admin"
              password: "${SONAR_PASSWORD}"
          - usernamePassword:
              scope: GLOBAL
              id: "dockerhubcreds"
              username: "${DOCKER_USER}"
              password: "${DOCKER_PASSWORD}"

unclassified:
  sonarGlobalConfiguration:
    installations:
      - name: "SonarQube"
        serverUrl: "http://localhost:9000"
        credentialsId: "sonar-credentials"

jenkins:
  clouds:
    - docker:
        name: "docker-cloud"
        dockerApi:
          dockerHost:
            uri: "tcp://host.docker.internal:2375"
        templates:
          - labelString: "DOTNETCORE"
            dockerTemplateBase:
              image: "username/agent-dnc:v1"
              mounts:
                - readOnly: false
                  type: BIND
            remoteFs: "/var/jenkins_home"
          - labelString: "DOCKER"
            dockerTemplateBase:
              image: "agent-docker:v1"
              privileged: true
            remoteFs: "/var/jenkins_home"

8. Appendix: Principles and good practices

Docker Principles

  1. Minimum base images — Reduce attack surface and footprint
  2. Avoid mounting untrusted sources via Docker socket
  3. Avoid --privileged mode unless absolutely necessary (Docker-in-Docker)
  4. No state in containers — State goes to volumes or version control
  5. Minimum trust — Check what external images/Dockerfiles are doing

Jenkins Principles

  1. Jenkinsfiles in version control — Most important asset
  2. Minimum plugin profile — Each plugin = attack surface + stability risk
  3. Ephemeral agents — No persistent workspace = clean and reproducible builds
  4. If something hurts, do it often — Fundamental DevOps principle (see upgrades)
  5. Restart to upgrade — Restarting Jenkins container triggers LTS upgrade

Security principles

  1. Hanlon’s Razor: Never attribute to malice what is explained by negligence
  2. Minimum trust: Trust as little as possible
  3. Minimal attack surface: Lightweight images, minimal plugins
  4. Encryption of sensitive data: Consider TLS/SSH for traffic with credentials
  5. Credential helpers: Avoid clear passwords in workspaces
┌─────────────────────────────────────────────────┐
│  Contrôle de version (Git)                      │
│  - Jenkinsfiles                                 │
│  - Dockerfiles des agents                       │
│  - Fichiers YAML JCasC                         │
└────────────────────┬────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────┐
│  Jenkins Master (conteneur)                     │
│  - Image : jenkins/jenkins:lts                  │
│  - Volume : /var/jenkins_home → NAS/stockage    │
│  - Port : 2119:8080                             │
└────────────────────┬────────────────────────────┘
                     │ API Docker Remote (port 2375)
                     ▼
┌─────────────────────────────────────────────────┐
│  Docker Host                                    │
│  - Lance les agents à la demande                │
│  - Détruit les agents après le build            │
└──────┬──────────────┬────────────────────┬──────┘
       ▼              ▼                    ▼
 [Agent DOTNETCORE] [Agent DOCKER] [Agent générique]
 (éphémère)         (éphémère)     (éphémère)


Search Terms

running · jenkins · docker · ci/cd · git · devops · image · agent · dockerfile · jenkinsfile · container · images · dotnetcore · agents · .net · core · multi-architecture · upgrade · buildx · commands · containers · multi-arch · principles · approach

Interested in this course?

Contact us to book it or get a custom training plan for your team.