Set up Docker Containers as a Build Agent for Jenkins

In this article, I am going to implement docker containers as a build agent, which distributes the Jenkins load to ephemeral containers. The default installation of Jenkins has only one agent called “master”, which is responsible for executing all jobs. If we have many jobs requiring lots of resources, then the master may not be enough to execute the jobs successfully. In this scenario, we have to create agents that take responsibility for executing the jobs. The agents can be anywhere, running locally or in the network, but should be accessible from the Jenkins master.

Pre-requisite

  1. We need Jenkins installed, up and running. We can install it in the system directly using the war file provided, or using Docker. I prefer the Docker method here.
  2. We need a host where the agent containers should be running. The updated Docker version should be installed.

Configure a Docker Host with Remote API

This is an important step. We have prepared the host and installed the docker. This host is contacted by the Jenkins server to run the build agent containers. I am using the Ubuntu server, and we can use any server supporting Docker. The master communicates with the Docker host using the REST API, and for that, we have to enable the remote API.

The port we use for the API is 4243, which should not be blocked by the firewall. We also need some ports (32768 to 60999), not to be blocked.

So, to enable the API, we log in to the Docker host and open the Docker service file.

sudo vi /lib/systemd/system/docker.service

Then we have to replace the line starting with “ExecStart=” with the following (in [Service] block)

ExecStart=
ExecStart=/usr/bin/dockerd \
         -H fd:// \
         --containerd=/run/containerd/containerd.sock \
         -H tcp://0.0.0.0:2375

Then, we have to reload and restart the docker service.

sudo systemctl daemon-reload
sudo service docker restart

The validation is done using the following curl command:

ss -tulnp | grep 2375 # checks if the port 2375 is listening
curl http://localhost:4243/version
curl http://[IP]:4243/version

Create a Jenkins Agent Docker Image

The second important step is to create a Docker image for the Jenkins agent. We can directly used the image (jenkins/ssh-agent) provided by the docker hub (Link).

But we prefer to extend this image so that we can customize it based on our requirements. Here is an example of how we can extend it:

Dockerfile

FROM jenkins/ssh-agent:debian-jdk17 as ssh-agent
LABEL maintainer="XXX"
LABEL tag="[user]/[image]:[tag]"

RUN apt update && \   
    #apt install -y gradle && \
    rm -rf /var/lib/apt/lists/*
WORKDIR /home/jenkins
ENTRYPOINT ["setup-sshd"]

We create an image from this Dockerfile and push it into a repository. We need to provide this image while creating a cloud agent in Jenkins.

Configure Jenkins Server With Docker Plugin

Now that Docker is listening, we need to tell Jenkins where to find it.

  1. Install the Plugin: Go to Manage Jenkins > Plugins and install the Docker plugin (provided by docker-java).
  2. Add the Cloud:
    • Navigate to Manage Jenkins > Clouds > New Cloud.
    • Select Docker.
    • Name: Give it a name like docker-prod.
    • Docker Host URI: Enter tcp://192.168.1.253:2375.
    • Test Connection: Click the button. You should see the Docker version returned immediately.
3. Create the Agent Template

In the same Cloud configuration page, add a Docker Template. This defines “what” the agent looks like.

  • Labels: docker-agent ( This is the tag you will use in your Pipeline.)
  • Name: my-agent.
  • Docker Image: jenkins/agent:latest (or your own custom image).
  • Remote File System Root: /home/jenkins.
  • Connect Method: Select “Attach Docker Container.

4. Verify with a “Hello World.”

Create a simple Pipeline job to verify the connection.

pipeline {
    agent { label 'docker-agent' }
    stages {
        stage('Test') {
            steps {
                sh 'echo "Hello from inside the container!"'
                sh 'hostname' 
            }
        }
    }
}

volume mapping to make the build faster.

The Security Gap

At this stage, you have a working system, but it is dangerous.

By exposing port 2375 unencrypted, you have given root access to that machine to anyone on your network. A malicious actor could easily mount the host’s filesystem and take control. To fix this, we must enable TLS.


Phase 2: Securing with TLS (HTTPS)

Goal: Encrypt traffic and force authentication using client certificates on the secure port 2376.

1. Generate the Certificates

We need a Certificate Authority (CA) to sign keys for both the Server (Docker Host) and the Client (Jenkins).

Run the following script on your Docker Host. (Save as generate_certs.sh, make executable with chmod +x, and run it).

Bash

#!/bin/bash
HOST="192.168.1.253" # <--- UPDATE THIS TO YOUR IP
PASSWORD="mypassword"
DIR="docker-certs"

mkdir -p $DIR && cd $DIR

# 1. Generate CA
echo "Generating CA..."
openssl genrsa -aes256 -passout pass:$PASSWORD -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem -passin pass:$PASSWORD -subj "/CN=$HOST"

# 2. Server Key (For Docker Daemon)
echo "Generating Server Keys..."
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr
echo "subjectAltName = DNS:$HOST,IP:$HOST,IP:127.0.0.1" > extfile.cnf
echo "extendedKeyUsage = serverAuth" >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile extfile.cnf -passin pass:$PASSWORD

# 3. Client Key (For Jenkins)
echo "Generating Client Keys..."
openssl genrsa -out key.pem 4096
openssl req -subj '/CN=client' -new -key key.pem -out client.csr
echo "extendedKeyUsage = clientAuth" > extfile-client.cnf
openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile extfile-client.cnf -passin pass:$PASSWORD

# Cleanup
rm -f client.csr server.csr extfile.cnf extfile-client.cnf
chmod 0400 ca-key.pem key.pem server-key.pem
chmod 0444 ca.pem server-cert.pem cert.pem

echo "Success! Certificates generated in $DIR"

2. Configure Docker to Enforce TLS

Now we apply these keys to the Docker daemon.

  1. Move Keys: Copy the server keys to a permanent directory:Bashsudo mkdir -p /etc/docker/certs sudo cp docker-certs/{ca.pem,server-cert.pem,server-key.pem} /etc/docker/certs/
  2. Edit Systemd: Open the override file again (sudo systemctl edit docker.service) and update the command. We are switching to port 2376 and adding the TLS flags:
  3. ExecStart= ExecStart=/usr/bin/dockerd \ --tlsverify \ --tlscacert=/etc/docker/certs/ca.pem \ --tlscert=/etc/docker/certs/server-cert.pem \ --tlskey=/etc/docker/certs/server-key.pem \ -H fd:// -H tcp://0.0.0.0:2376
  4. Restart: sudo systemctl restart docker.

3. Update Jenkins Credentials

Finally, give Jenkins the keys it needs to authenticate.

  1. Retrieve Keys: Copy ca.pem, cert.pem, and key.pem from the Docker host to your local computer.
  2. Add Credentials:
    • In Jenkins, go to Manage Jenkins > Credentials > System > Global credentials > Add Credentials.
    • Kind: Select X.509 Client Certificate.
    • Client Key: Paste the content of key.pem.
    • Client Certificate: Paste the content of cert.pem.
    • Server CA Certificate: Paste the content of ca.pem.
    • ID: docker-tls-auth.
  3. Update Cloud Config:
    • Go back to your Docker Cloud settings.
    • Docker Host URI: Change this to tcp://192.168.1.253:2376 (Note the secure port).
    • Server Credentials: Select the docker-tls-auth ID you just created.
    • Test Connection: It should succeed!

Conclusion

You now have a production-ready Jenkins architecture. Your build agents are dynamically provisioned on a remote host, and all traffic is fully encrypted and authenticated. This setup scales easily—as your team grows, you can simply add more powerful Docker hosts without changing your pipeline code.

References:

https://devopscube.com/docker-containers-as-build-slaves-jenkins/

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *