Intermediate 3 min · June 21, 2026

Jenkins Docker Integration: Build Pipelines That Don't Burn Down at 3 AM

Jenkins Docker integration done right.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
June 21, 2026
last updated
1,577
articles · all by Naren
 ● Production Incident 🔎 Debug Guide
Quick Answer

To integrate Docker with Jenkins, install the Docker Pipeline plugin, then use docker.image('image').inside() in your Jenkinsfile to run steps inside a container. For dynamic agents, use the docker agent directive with agent { docker { image 'node:18' } }. Always pin image tags, never use latest.

✦ Definition~90s read
What is Jenkins Docker Integration?

Jenkins Docker integration lets you run each build step inside a fresh Docker container, giving you isolated, reproducible environments without managing a fleet of static build slaves. Jenkins spins up containers on demand, runs your pipeline, then destroys them.

Imagine you're a chef who needs to cook different cuisines.
Plain-English First

Imagine you're a chef who needs to cook different cuisines. Instead of having a separate kitchen for each cuisine (static build slaves), you have one kitchen that you can instantly repurpose by swapping out the countertops, pans, and ingredients (Docker containers). Each recipe gets a fresh, perfectly equipped kitchen, and when you're done, you clean it completely. No cross-contamination, no waiting for a specific kitchen to free up.

Everyone thinks Dockerizing Jenkins is about 'consistency' and 'reproducibility.' That's the marketing fluff. The real reason is survival: static build slaves are ticking time bombs. I've watched a team's Jenkins master grind to a halt because one rogue build filled up the slave's disk with node_modules. With Docker, that build dies in isolation and the next one starts clean. This article covers the patterns that actually work in production — not the hello-world examples. You'll learn how to set up dynamic Docker agents, cache dependencies without leaking disk, and debug the inevitable 'container exited with code 137' at 3 AM.

Why Static Build Slaves Are a Legacy Anti-Pattern

Before Docker, Jenkins slaves were long-lived VMs or bare metal. You'd SSH in, install JDK 8, Maven, Node 12, Python 3.6 — and pray no build left behind a zombie process or filled the disk. The problem? State leaks. One build's npm install pollutes the next. A stuck mvn test eats all RAM. You end up with 'snowflake' slaves that can't be reproduced. Docker fixes this by giving each build a fresh container. But the real win is elasticity: you can spin up 20 containers on a single beefy host, or spread across a swarm/K8s cluster. No more 'waiting for executor' because a slave is busy with a different project.

JenkinsfileDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — DevOps tutorial

pipeline {
    agent none
    stages {
        stage('Build') {
            agent {
                docker {
                    image 'maven:3.8.6-eclipse-temurin-11'
                    args '-v /tmp/maven-cache:/root/.m2 --memory=2g'
                }
            }
            steps {
                sh 'mvn clean package'
            }
        }
    }
}
Output
Running on Jenkins in /workspace/...
[Pipeline] { (Build)
[Pipeline] withDockerContainer
Jenkins seems to be running inside container...
$ docker run -t -d -u 1000:1000 -v /tmp/maven-cache:/root/.m2 --memory=2g maven:3.8.6-eclipse-temurin-11 cat
$ docker exec ... mvn clean package
[INFO] Scanning for projects...
[INFO] Building my-app 1.0-SNAPSHOT
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ my-app ---
[INFO] --- maven-package-plugin:3.2.0:package (default-package) @ my-app ---
[INFO] Building jar: target/my-app-1.0-SNAPSHOT.jar
[INFO] BUILD SUCCESS
[Pipeline] }
Production Trap: Volume Mount Permissions
When mounting host directories into containers, the UID inside the container must match the host UID of the Jenkins user. If they differ, you get 'Permission denied' on writes. Fix by using args '-u <uid>:<gid>' or set the image's user to match via Dockerfile.

Dynamic Docker Agents: Spinning Up Ephemeral Workers

The simplest integration is the docker agent — Jenkins creates a container for the entire pipeline or per stage. But for heavy builds, you want per-stage containers to avoid image bloat. The Docker Pipeline plugin gives you docker.image('image').inside() for fine-grained control. Why would you use this? When your build needs Node 18, but your test stage needs Python 3.10. With static slaves, you'd need both installed. With Docker, each stage gets its own image. The trade-off: pulling images adds latency. Mitigate with a local registry mirror or pre-pull images on the host.

JenkinsfileDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — DevOps tutorial

pipeline {
    agent any
    stages {
        stage('Install Dependencies') {
            steps {
                script {
                    docker.image('node:18-alpine').inside('-v /tmp/npm-cache:/root/.npm') {
                        sh 'npm ci'
                    }
                }
            }
        }
        stage('Test') {
            steps {
                script {
                    docker.image('python:3.10-slim').inside {
                        sh 'pip install -r requirements.txt && pytest'
                    }
                }
            }
        }
    }
}
Output
[Pipeline] { (Install Dependencies)
[Pipeline] script
[Pipeline] docker.image
[Pipeline] inside
$ docker run -t -d -u 1000:1000 -v /tmp/npm-cache:/root/.npm node:18-alpine cat
$ docker exec ... npm ci
added 1250 packages in 12s
[Pipeline] }
[Pipeline] { (Test)
[Pipeline] script
[Pipeline] docker.image
[Pipeline] inside
$ docker run -t -d -u 1000:1000 python:3.10-slim cat
$ docker exec ... pip install -r requirements.txt && pytest
...
collected 42 items
...
42 passed in 3.5s
[Pipeline] }
Senior Shortcut: Cache Volumes
Use host-mounted volumes for package caches (npm, maven, pip). This cuts build time by 70% on the second run. But beware: if the cache gets corrupted, you'll chase phantom failures. Add a stage('Clear Cache') with a manual trigger to wipe it.

Docker-in-Docker: The Pattern That Works (and the One That Doesn't)

Sometimes your build needs to build Docker images itself — a CI pipeline that produces a Docker image. The naive approach is Docker-in-Docker (DinD): run a container with Docker inside. Don't. It's a security nightmare and requires privileged mode. Instead, mount the host's Docker socket into the container. This is called 'Docker-out-of-Docker' (DooD). The container talks to the host's Docker daemon. It's simpler, faster, and doesn't need privileged mode. The catch: containers spawned by the build appear on the host, not inside the container. That's fine — they're ephemeral anyway.

JenkinsfileDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — DevOps tutorial

pipeline {
    agent any
    stages {
        stage('Build Docker Image') {
            steps {
                script {
                    docker.image('docker:20.10.16-dind').inside(
                        '-v /var/run/docker.sock:/var/run/docker.sock'
                    ) {
                        sh 'docker build -t myapp:${BUILD_NUMBER} .'
                    }
                }
            }
        }
    }
}
Output
[Pipeline] { (Build Docker Image)
[Pipeline] script
[Pipeline] docker.image
[Pipeline] inside
$ docker run -t -d -u 1000:1000 -v /var/run/docker.sock:/var/run/docker.sock docker:20.10.16-dind cat
$ docker exec ... docker build -t myapp:123 .
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM alpine:3.16
---> 6dbb9cc54074
Step 2/3 : COPY . /app
---> 8a2b3c4d5e6f
Step 3/3 : CMD ["/app/run.sh"]
---> Running in 9a8b7c6d5e4f
Removing intermediate container 9a8b7c6d5e4f
---> f1e2d3c4b5a6
Successfully built f1e2d3c4b5a6
Successfully tagged myapp:123
[Pipeline] }
Never Do This: Privileged DinD
Running docker run --privileged -v /var/run/docker.sock:/var/run/docker.sock is a root escape waiting to happen. If you need DinD (e.g., testing Docker-in-Docker), use --security-opt seccomp=unconfined instead of --privileged, and run the container with a non-root user.

Caching Dependencies Without Blowing Up Disk

The biggest pain with Docker in CI is image pull times and cache misses. You want to cache node_modules, .m2, pip packages, etc. The naive approach: mount a host volume. That works, but the cache grows unbounded. I've seen /tmp/npm-cache hit 50GB. Set a size limit with Docker's --storage-opt or use a cron job to prune old caches. Better: use a dedicated cache volume with docker volume create --driver local --opt type=none --opt device=/data/cache --opt o=bind cache_vol. Then mount it with -v cache_vol:/cache. This gives you control over the backing filesystem and can be backed up.

JenkinsfileDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — DevOps tutorial

pipeline {
    agent any
    environment {
        CACHE_VOL = 'maven-cache'
    }
    stages {
        stage('Build with Cache') {
            steps {
                script {
                    // Create cache volume if not exists
                    sh 'docker volume inspect ${CACHE_VOL} || docker volume create ${CACHE_VOL}'
                    docker.image('maven:3.8.6-eclipse-temurin-11').inside(
                        "-v ${CACHE_VOL}:/root/.m2 --memory=2g"
                    ) {
                        sh 'mvn clean package'
                    }
                }
            }
        }
    }
}
Output
[Pipeline] { (Build with Cache)
[Pipeline] script
[Pipeline] sh
+ docker volume inspect maven-cache
Error: No such volume: maven-cache
+ docker volume create maven-cache
maven-cache
[Pipeline] docker.image
[Pipeline] inside
$ docker run -t -d -u 1000:1000 -v maven-cache:/root/.m2 --memory=2g maven:3.8.6-eclipse-temurin-11 cat
$ docker exec ... mvn clean package
[INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ my-app ---
[INFO] Building jar: target/my-app-1.0-SNAPSHOT.jar
[INFO] BUILD SUCCESS
[Pipeline] }
The Classic Bug: Stale Cache
A cached node_modules can mask breaking changes in dependencies. Always run npm ci (which respects lockfile) instead of npm install. For Maven, use -o (offline) after the first build to force cache usage — but clear cache when pom.xml changes.

When Docker Agents Break: Debugging Container Failures

Containers fail silently. The most common error is exit code 137 (OOMKilled). You won't see it in the build logs — just 'Process exited with code 137'. Check dmesg | grep -i oom on the host. Another: exit code 139 (segfault) — usually a memory corruption or incompatible library. Exit code 1 is a script error. But the worst is exit code 0 with no output — the container started and immediately exited because the entrypoint ran and finished. This happens when you use docker run without a long-running process. Jenkins uses cat as the entrypoint to keep the container alive. If you override entrypoint, the container exits immediately.

debug.shDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — DevOps tutorial

# Check OOM kills on the Docker host
sudo dmesg | grep -i oom | tail -20

# Check Docker container exit codes
# In Jenkins pipeline, add this step to debug:
sh 'docker inspect $(docker ps -aq --filter name=jenkins-) --format "{{.State.ExitCode}}"'

# Test if an image can run interactively
docker run --rm -it alpine:3.16 sh -c 'echo "Container works"; exit 0'

# If using custom entrypoint, ensure it doesn't exit
# Bad: CMD ["build.sh"]  # exits after build
# Good: CMD ["sh", "-c", "build.sh && tail -f /dev/null"]
Output
[ 123.456] oom-kill: constraint=CONSTRAINT_MEMCG, process=node, pid=7890, total-vm=2048000kB, anon-rss=1800000kB, file-rss=0kB, shmem-rss=0kB
0
Container works
Interview Gold: Exit Code 137
Interviewers love asking about exit code 137. It's the OOM killer. The fix is always memory limits or reducing the build's memory footprint. Never say 'increase swap' — that hides the problem and kills performance.
● Production incidentPOST-MORTEMseverity: high

The 4GB Container That Kept Dying

Symptom
A Node.js build pipeline consistently failed with 'Process exited with code 137' after 2 minutes of npm install. No error logs before the exit.
Assumption
Team assumed npm was hitting a network timeout or a corrupted cache.
Root cause
The default Docker container memory limit on the Jenkins agent was 2GB. npm install for a monorepo with 50 packages needed 3.5GB. The kernel OOM killer silently terminated the container — no npm error, just a 137 exit code.
Fix
Added args '--memory=4g' to the Docker agent directive in the Jenkinsfile. Also set --memory-swap=4g to disable swap (swap kills performance).
Key lesson
  • Always set explicit memory limits on Docker containers in Jenkins — the default is often too low for modern builds.
  • Monitor OOM kills with dmesg | grep -i oom.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Pipeline hangs on 'Still waiting to schedule task'
Fix
1. Check Jenkins executor availability: http://jenkins/computer/. 2. Check Docker host resources: docker info | grep -E 'Containers|Images|Memory'. 3. If memory < 2GB free, run docker system prune -f to clean up. 4. Restart Jenkins agent if needed.
Symptom · 02
Container exits with code 137 (OOMKilled)
Fix
1. Check host OOM logs: sudo dmesg | grep -i oom. 2. Increase container memory: add args '--memory=4g' to agent directive. 3. Reduce build memory usage: e.g., for Node.js, set NODE_OPTIONS=--max_old_space_size=2048. 4. If still failing, add swap cautiously: --memory-swap=6g.
Symptom · 03
Pipeline fails with 'docker: not found' inside container
Fix
1. You're trying to run Docker commands inside a container that doesn't have Docker CLI. 2. Use an image with Docker CLI: docker:20.10.16-dind. 3. Mount the Docker socket: args '-v /var/run/docker.sock:/var/run/docker.sock'. 4. Ensure the container user has permission to access the socket (usually group 999).
Feature / AspectStatic SlaveDocker Agent
Environment isolationNone — builds share filesystemFull — each build gets fresh container
Setup timeHours to provision VMSeconds to pull image
Resource utilizationFixed — idle slaves waste resourcesElastic — containers created on demand
State leakageCommon — zombie processes, disk fillNone — container destroyed after build
CachingPersistent on slave diskRequires volume mounts or external cache
SecuritySSH keys, long-lived credentialsEphemeral — credentials injected per build
DebuggingSSH into slavedocker exec -it <container> sh

Key takeaways

1
Always pin Docker image tags
never use latest in production pipelines.
2
Set explicit memory limits on Docker containers; the default is often too low and causes silent OOM kills (exit code 137).
3
Use Docker-out-of-Docker (mount the host socket) instead of privileged Docker-in-Docker for building images inside containers.
4
Cache dependencies via host-mounted volumes or named volumes, but set size limits and prune regularly to avoid disk bloat.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

FAQ · 4 QUESTIONS

Frequently Asked Questions

01
How do I run a Jenkins pipeline inside a Docker container?
02
What's the difference between Docker Pipeline plugin and Docker plugin in Jenkins?
03
How do I pass credentials to a Docker container in Jenkins?
04
Why does my Jenkins Docker build fail with 'Cannot connect to the Docker daemon'?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
June 21, 2026
last updated
1,577
articles · all by Naren
🔥

That's Jenkins. Mark it forged?

3 min read · try the examples if you haven't

Previous
Jenkins Maven Build
14 / 23 · Jenkins
Next
Jenkins Kubernetes Deployment