Running Containerized Jenkins Pipelines with Full Host Isolation via Docker-in-Docker (DinD)
Incorporating containerization in CI/CD pipelines has become essential, as using Docker to build and test applications helps prevent unexpected behaviors while allowing full customization over the execution environment.
However, as of today, many articles promoting the use of containerized Jenkins pipelines suggest solutions that pose significant security risks by granting unrestricted root access to the host system through Docker socket mounts. This is particularly concerning for organizations where system administrators may not have complete control over who runs what during builds.
In this post, you’ll learn how to execute Docker CLI commands through the Docker-in-Docker (DinD) daemon within your Jenkins pipelines. This approach serves as an alternative to the traditional method of creating containers on the top-level Docker host.
Why Docker-in-Docker?
-
Host isolation π
-
A secure TCP connection is established to grant access to the Docker daemon running within the DinD service. Running DinD as a sidecar ensures complete isolation between the host system and the Jenkins container by avoiding the common, yet very dangerous, practice of exposing the Docker socket (
docker.sock
) via a bind mount. The socket is owned by root, so granting access to it effectively provides unrestricted root privileges over any operations the Docker service can perform.
-
-
No custom builds required β
-
To eliminate the need for custom builds, Docker CLI binaries and plugins are mounted into the Jenkins container as read-only shared volumes. The official jenkins/jenkins image does not include these binaries by default.
-
-
Automated Jenkins updates π
-
By not relying on custom builds of Jenkins, we can easily automate updates based on official releases, using services designed for this purpose, such as containrrr/watchtower. Maintaining a custom image of Jenkins consistently up-to-date requires frequent builds, as updates get often released.
-
Setting up Jenkins with a DinD sidecar
Enough rambling, let’s jump right into the solution!
Here’s the complete docker-compose.yml
file:
# docker compose up -d
services:
jenkins:
image: jenkins/jenkins:jdk21
environment:
- DOCKER_HOST=tcp://docker:2376 (1)
- DOCKER_CERT_PATH=/certs/client
- DOCKER_TLS_VERIFY=1
depends_on: [ dind ]
ports: [ 8080:8080, 50000:50000 ]
networks: [ dind-network ]
restart: unless-stopped
volumes:
- jenkins-data:/var/jenkins_home
# Read-only shared volumes (2)
- dind-bin:/usr/local/bin/dind:ro
- dind-plugins-bin:/usr/local/libexec/docker/cli-plugins:ro
- dind-certs-client:/certs/client:ro
entrypoint: /bin/sh -c 'PATH="/usr/local/bin/dind:$${PATH}" exec tini -- jenkins.sh' (3)
dind:
image: docker:dind
networks:
dind-network:
aliases: [ docker ]
restart: unless-stopped
privileged: true (4)
volumes:
- dind-bin:/usr/local/bin
- dind-plugins-bin:/usr/local/libexec/docker/cli-plugins
- dind-certs-client:/certs/client
- dind-data:/var/lib/docker
- jenkins-data:/var/jenkins_home
volumes:
jenkins-data:
dind-bin:
dind-plugins-bin:
dind-certs-client:
dind-data:
networks:
dind-network:
driver: bridge
1 | Uses docker as the DinD network alias for encrypted communications with the daemon. |
2 | Mounts shared volumes as read-only (ro ) to restrict the ability to rewrite binaries in the source container. |
3 | I know what you’re thinking, this does look a bit hacky, but all it does is add the DinD binaries directory to the PATH variable before starting Jenkins. |
4 | Enabling the privileged [1] property is necessary to allow the inner Docker daemon to function properly. |
Docker Compose allows us to organize complex multi-container setups within a single, easy-to-read file.
This way, users can start Jenkins with DinD by simply running the command: docker compose up -d
.
Running build steps inside a DinD container
Starting a DinD container as the execution environment for your pipeline builds couldn’t be simpler!
You can use DinD in the same way as you would with the top-level Docker, but first, make sure to have the Docker Pipeline plugin installed. Jenkins will automatically start the specified container and execute the defined steps within.
pipeline {
agent any
stages {
stage('Run DinD') {
agent {
docker {
image 'python:3'
args '--network host' (1)
reuseNode true (2)
}
}
steps {
sh '''python -c "import os; os.system('curl https://example.com')"''' (3)
}
}
}
}
1 | Helps prevent networking issues by enabling data transmission over the host network of the DinD daemon. |
2 | Starts the container on the same node workspace, rather than on a new node entirely. |
3 | Runs a small Python script that uses curl to fetch and display the content from the example.com website. |
Note the --network host
argument.
Without it, you may run into some network issues due to the container inception going on.
Bear in mind, we’re starting a new container inside the existing DinD service container.
Building and running a DinD container from a Dockerfile
For a more customized execution environment, Jenkins pipelines also support building and running a container from a Dockerfile located in the source repository. With this approach, a new image is built instead of retrieving one from a Docker registry.
pipeline {
agent any
stages {
stage('Build & Run DinD') {
agent {
dockerfile {
dir './path/to/dockerfile/dir'
additionalBuildArgs '--network host' (1)
args '--network host'
reuseNode true
}
}
steps { ... }
}
}
}
1 | Helps prevent networking issues by enabling data transmission over the DinD daemon’s host network for RUN instructions during build. |
Conclusion and future work
In this post, we discussed how to make the most of Docker-in-Docker as a sidecar container to keep Jenkins execution environments fully isolated from the host system, while eliminating the need for custom builds to include Docker binaries.
In case you’re considering transitioning your existing Jenkins setup to the one described, it’s worth checking what problems you may run into when adopting Docker-in-Docker. I highly recommend reading JΓ©rΓ΄me Petazzoni’s article [2], as it goes through the pros and cons of the various options available for running Docker from your Jenkins CI system.
Furthermore, the proposed DinD sidecar solution relies on a single host machine with limited resources available, as we opted for local agents to handle job scheduling. For larger workloads, please consider remote SSH agents or Kubernetes to achieve optimal scalability.
Future developments or improvements to this solution may involve: