We’ve been running Jenkins for CI/CD for years, but builds were inconsistent. “Works on my machine” was a daily occurrence. Docker solved this.

The Problem

Our Jenkins agents had accumulated cruft over time:

  • Multiple Java versions installed
  • Random npm packages globally installed
  • Different versions of build tools on different agents
  • Builds would pass on one agent, fail on another

Sound familiar?

The Solution: Docker

Run each build in a fresh Docker container. Every build gets a clean, reproducible environment.

Setting It Up

1. Install Docker Plugin

In Jenkins, install the “Docker Plugin” and “Docker Pipeline Plugin”.

2. Configure Docker Cloud

Go to Manage Jenkins → Configure System → Cloud → Add a new cloud → Docker.

Point it to your Docker daemon (we’re using a remote Docker host):

Docker Host URI: tcp://docker-host:2376

3. Create a Build Image

We created a base image with our build tools:

FROM ubuntu:16.04

# Install Java
RUN apt-get update && apt-get install -y \
    openjdk-8-jdk \
    maven \
    git \
    curl

# Install Node.js
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - \
    && apt-get install -y nodejs

# Install Go
RUN curl -O https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz \
    && tar -xvf go1.7.linux-amd64.tar.gz \
    && mv go /usr/local

ENV GOROOT=/usr/local/go
ENV GOPATH=/go
ENV PATH=$PATH:$GOROOT/bin:$GOPATH/bin

WORKDIR /workspace

Build and push to our private registry:

docker build -t registry.example.com/jenkins-agent:latest .
docker push registry.example.com/jenkins-agent:latest

4. Update Jenkinsfile

Use the Docker image in your pipeline:

pipeline {
    agent {
        docker {
            image 'registry.example.com/jenkins-agent:latest'
            args '-v /var/run/docker.sock:/var/run/docker.sock'
        }
    }
    
    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package'
            }
        }
        
        stage('Test') {
            steps {
                sh 'mvn test'
            }
        }
    }
}

Now every build runs in a fresh container with exactly the tools we need.

The Benefits

1. Reproducible Builds

Every build uses the same environment. No more “works on my machine” or “works on agent-1 but not agent-2”.

2. Easy to Update Tools

Need to upgrade Maven? Update the Dockerfile, rebuild the image, and all builds use the new version. No need to SSH into agents and update manually.

3. Parallel Builds

We can run multiple builds in parallel without conflicts. Each gets its own container.

4. Faster Agent Provisioning

Need more build capacity? Spin up another Docker host. No need to manually configure a new Jenkins agent.

The Challenges

1. Docker-in-Docker

Some of our builds create Docker images. Running Docker inside Docker is tricky. We mount the Docker socket:

agent {
    docker {
        image 'jenkins-agent:latest'
        args '-v /var/run/docker.sock:/var/run/docker.sock'
    }
}

This lets the container use the host’s Docker daemon. Not perfect, but it works.

2. Build Time

Pulling the Docker image adds overhead. First build takes longer. We mitigate this by:

  • Keeping images small
  • Using a local registry
  • Pre-pulling images on agents

3. Caching

Maven and npm downloads are slow. We mount cache directories:

args '-v maven-cache:/root/.m2 -v npm-cache:/root/.npm'

This persists dependencies between builds.

Performance Comparison

Before (traditional agents):

  • Build time: 8 minutes
  • Setup time: 0 (agent already configured)
  • Consistency: Low (different agents, different results)

After (Docker):

  • Build time: 8 minutes
  • Setup time: 30 seconds (pull image)
  • Consistency: High (same environment every time)

The 30-second overhead is worth it for the consistency.

Advanced: Multi-Stage Builds

For projects with different requirements, we use different images:

pipeline {
    agent none
    
    stages {
        stage('Build Backend') {
            agent {
                docker { image 'java-build:latest' }
            }
            steps {
                sh 'mvn package'
            }
        }
        
        stage('Build Frontend') {
            agent {
                docker { image 'node-build:latest' }
            }
            steps {
                sh 'npm run build'
            }
        }
    }
}

Each stage uses the appropriate image.

Tips

  1. Keep images small: Use Alpine Linux as base when possible
  2. Version your images: Tag with version numbers, not just latest
  3. Cache dependencies: Mount cache directories for package managers
  4. Pre-pull images: Pull images on agents before builds start
  5. Clean up: Set up a cron job to remove old containers and images

What’s Next

We’re exploring:

  • Kubernetes for running Jenkins agents
  • Building images with BuildKit for better caching
  • Using multi-stage Dockerfiles to reduce image size

The Bottom Line

Docker + Jenkins has been a game-changer for us. Builds are consistent, agents are easy to manage, and we can scale easily.

If you’re still running builds on bare metal or VMs, give Docker a try. The initial setup takes some work, but the payoff is huge.

Anyone else using Docker with Jenkins? What’s your setup like?