Table of Contents
- 2.1 Introduction
- 2.2 Docker and the Kernel
- 2.3 Demo: A Linux container on Windows 10
- 2.4 What is a Kernel?
- 2.5 Why it matters: running Linux containers on Windows
- 2.6 The vision and the why: Jenkins on Docker
- 2.7 Demo: The basics of running Jenkins in a container
- 2.8 Maintain state outside container
- 2.9 The Docker Filesystem
- 2.10 Understanding Copy-on-Write
- 2.11 Demo: Mount a volume on your container
- 2.12 Jenkins Status Conclusion
- 2.13 Section Summary
- 3.1 Introduction
- 3.2 Demo: Provisioning containerized agents
- 3.3 What’s happening with Cloud Agents
- 3.4 Understanding Docker Images and Trust
- 3.5 Demo: Create a DotNetCore agent with a Dockerfile
- 3.6 Demo: Attach DotNetCore agent to your cloud
- 3.7 Conclusion on DotNetCore Agent
- 3.8 Demo: Create a Docker image meta-build
- 3.9 Meta-build considerations
- 3.10 Understanding Container Connection Methods
- 3.11 Working with private registers
- 3.12 Demo: Authentication to a custom registry
- 3.13 Install dependencies dynamically
- 3.14 Working with ephemeral agents
- 3.15 Demo: Working with artifacts on ephemeral agents
- 3.16 More specific Jenkins images
- 3.17 Section Summary
- 4.1 Understanding multi-architecture
- 4.2 Demo: Multi-architecture negotiation in Docker
- 4.3 How BuildX builds for unavailable platforms
- 4.4 Demo: Building multi-architecture Docker images
- 4.5 Build with BuildKit
- 4.6 Demo: Building multi-architecture images in Jenkins-Docker
- 4.7 Embed this in Jenkins
- 4.8 Demo: Getting this to work in Jenkins
- 4.9 Conclusion and perspectives
- 4.10 Section Summary
- 5.1 Introduction
- 5.2 Upgrade Jenkins
- 5.3 Demo: Maintain and upgrade Jenkins
- 5.4 Upgrade Plugins
- 5.5 Jenkins and Infrastructure as Code
- 5.6 Important points to consider
- 5.7 Course summary
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:
- In a Windows shell, the
systeminfocommand displays information confirming a Windows build (NT kernel). - Docker Desktop is used to launch the Jenkins LTS container.
- A shell window is open in the container.
- In the container, navigate to
/etcand read theos-releasefile:
# 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
| Type | Example | Description |
|---|---|---|
| Monokernel | Linux | All OS code runs in the same memory space |
| Microkernel | Minix | Only the minimum runs in privileged mode |
| Hybrid Kernel | Windows (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:
- A build is queued
- Azure reads the build definition and looks for requirements for agents (OS, dependencies, etc.)
- Azure matches the build with an agent
- Azure launches a matching VM to run the build
- A second build in parallel launches a completely independent process
- 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 updatejenkins/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
- Recover the initial administrator password:
docker exec jenkins-master cat /var/jenkins_home/secrets/initialAdminPassword - Install default plugins
- Create the administrator account (
cbehrensin the demo) - 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:
- A Jenkins configuration file needs to be modified
- The storage driver searches for this file in the image layers
- It copies this file to the TWL
- 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:
-
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.
-
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):
- Go to Docker Desktop Settings
- 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:
- Manage Jenkins → Manage Nodes and Clouds
- Configure Clouds
- Add a new Docker type Cloud
- Configure Docker URI:
tcp://host.docker.internal:2375 - 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:
- Launch an agent container
- Run build on it
- Destroy container when finished
3.3 What happens with Cloud Agents
Here is the complete sequence when triggering a build:
- Jenkins resolves label (or “any” if no label required)
- Jenkins first searches among its attached nodes — none in our case
- Jenkins consults his Docker cloud for this label, finding the corresponding template
- Jenkins instance phones home to Docker host via REST calls
- 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)
- Jenkins provisions this container as an agent node, communicates with it via Jenkins Remoting technology
- Build runs — success or failure
- Jenkins destroys container — process is complete
Two major limitations at this stage
-
Our agent has nothing installed. If we wanted to do a real build (not just Hello World), our simple image is not enough.
-
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:
FROM jenkins/jenkins:lts: base image — agent inherits from Jenkins LTSUSER root: we switch to root context for installations- APT update:
apt-get update && apt-get upgrade - Microsoft key: download
microsoft.asc, transform it withgpg --dearmorand place it in the trusted keys - Microsoft repository: we create
/etc/apt/sources.list.d/dotnetdev.listwith the contents of the Microsoft repository (deb [arch=amd64] <url> stretch main) - SDK installation:
apt-get install dotnet-sdk-3.1— automatically adds location to PATH - Checking:
dotnet --versionshows 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.Xinstruction in the Dockerfile. You can push the result as a new tag, for exampleagent-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
- Manage Jenkins → Manage Nodes and Clouds → Configure Clouds
- In the Cloud Docker configuration, add a new agent template:
- Docker Image:
username/agent-dnc:v1(oragent-dnc:v1if local) - Labels:
DOTNETCORE - Remote Filing System Root:
/var/jenkins_home
- 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 Jenkins → Manage Plugins → Available
- 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:ltsas base layer - We install
apt-transport-httpsandca-certificatesfor 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
jenkinsto thedockergroup 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.
Recommended validation process
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:
| Method | Description |
|---|---|
| Attach Container | Uses the Docker host’s ability to pass commands and data between the master and the agent image |
| JNLP | Java Network Launch Protocol (also called Web Start in Java) |
| SSH | Secure Shell |
Comparison of prerequisites
| Prerequisites | Attach Container | JNLP | SSH |
|---|---|---|---|
| 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
- Pushing to a private repository via a build — mostly already solved with
docker.withRegistry - 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
- Go to Docker Hub →
username/agent-dnc - Settings → make private
- 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 clonevery 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
rootis 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 archivefingerprint: calculate a SHA1 hash to track artifact usageonlyIfSuccessful: iftrue, only archives if the build succeeds (do not use for debugging)allowEmptyArchive: iftrue, 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:
- Go to the main build page
- You will see a list of archived files
- 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
-
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.
-
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 agentsjenkins/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:ltsfirst, then experiment with lighter images likejenkins/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:
- Simplest agent: built directly from
jenkins/jenkins:lts, executed a Hello World build - DotNetCore Agent: Dockerfile based on Jenkins image with .NET SDK installed
- Docker-in-Docker Agent: Docker running in a Docker Jenkins container to build and push images
- Private Connections: Connecting to private GitHub repositories and private Docker Hub
- 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-windowsourenterprise/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:
amd64andx86_64are 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--pushis 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:
| Appearance | Classic engine | BuildKit |
|---|---|---|
| Treatment | Serial | Parallel (as much as possible) |
| Cache | Basic | Based on Git-like heuristics |
| Performance | Standard | Significantly 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
buildxcommands 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:
Create a custom build space (→ already in the scriptcbbspace)Tell Docker to use this builder→ already in the script- Copy the Dockerfile into the container → offered for free by Jenkins job provisioning (SCM checkout)
- Create a credential set for Docker Hub authentication → in Jenkins via
docker.withRegistry - Run multi-arch build → in Jenkinsfile
- 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
- New Item → name
Build-SimpleDNC→ type Pipeline - Scroll to Pipeline → select Pipeline from SCM
- SCM: Git
- Repository URL: URL of your GitHub repository
- Script Path:
build-simple-dnc.jenkinsfile - 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
- Understanding the multi-architecture challenge
- The traditional approach vs. how BuildX solves this problem with QEMU
- Demonstrating a manual multi-arch build from the command line
- Docker-in-Docker with multi-arch builds experimental
- 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:
- You are not upgrading often enough
- Each individual upgrade is more painful
- This requires postponing subsequent upgrades
- 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:
- We work with
jenkins/jenkins:lts - At some point, the LTS version will change — a new version will be tagged
lts - The base of your Jenkins image will change
- 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:
- Configure a Jenkins instance (modifies
config.xmland other files in the volume) - For new instances (master or agent), base on this volume — create a copy and make modifications
- Store volume contents in version control
JCasC approach:
- Write configuration YAML files
- Store these YAML files in version control
- 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
| Risk | Mitigation |
|---|---|
| Docker socket mounted | Use only trusted images (Jenkins LTS), minimal Dockerfiles |
| Unencrypted traffic (port 2375) | Consider TLS or SSH for sensitive environments |
| Credentials in plain text in the workspace | Use Docker credential helpers |
| Plugins with vulnerabilities | Minimal profile + regular updates |
| Images with too much attack surface | Prefer 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
- Minimum base images — Reduce attack surface and footprint
- Avoid mounting untrusted sources via Docker socket
- Avoid
--privilegedmode unless absolutely necessary (Docker-in-Docker) - No state in containers — State goes to volumes or version control
- Minimum trust — Check what external images/Dockerfiles are doing
Jenkins Principles
- Jenkinsfiles in version control — Most important asset
- Minimum plugin profile — Each plugin = attack surface + stability risk
- Ephemeral agents — No persistent workspace = clean and reproducible builds
- If something hurts, do it often — Fundamental DevOps principle (see upgrades)
- Restart to upgrade — Restarting Jenkins container triggers LTS upgrade
Security principles
- Hanlon’s Razor: Never attribute to malice what is explained by negligence
- Minimum trust: Trust as little as possible
- Minimal attack surface: Lightweight images, minimal plugins
- Encryption of sensitive data: Consider TLS/SSH for traffic with credentials
- Credential helpers: Avoid clear passwords in workspaces
Recommended architecture (Jenkins on Docker)
┌─────────────────────────────────────────────────┐
│ 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