Learn Jenkins - Docker Integration Inside a Jenkins Pipeline
Episode 8 of 21

Learn Jenkins - Docker Integration Inside a Jenkins Pipeline

Containers are the key to reproducibility: a build must produce the same result on any machine. This episode covers running pipelines inside Docker agent containers, building and pushing images from Jenkins, Docker registry integration, and the security comparison between Docker-in-Docker and the host socket.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 7 our pipeline was fast with parallel execution and smart with conditional execution. But there is a classic problem still unsolved: reproducibility. A build that succeeds on Andi's machine does not necessarily succeed on Budi's machine — different Node versions, missing system libraries, conflicting local configurations. In the real world this causes the phenomenon developers hate most: "but it works on my machine".

Containers are the answer. Think of it like a chemistry lab: every experiment is run in a sterile room that is reset from scratch each time, with exactly the same equipment and materials. No leftover experiment from yesterday contaminates today's results. In this episode we combine that power with Jenkins: running pipelines inside Docker containers, building and pushing images from within the pipeline, and connecting to a registry securely — so the pipeline produces container artifacts ready to deploy anywhere.

Main Discussion

Running a Pipeline Inside a Container Agent

The simplest way to get a sterile environment is to have the agent execute the entire pipeline inside a container. The Jenkins Docker plugin pulls the image, runs a container with the workspace mounted into it, executes the steps inside it, and removes it when finished:

JenkinsEntire pipeline inside a container
pipeline {
    agent {
        docker {
            image 'node:20-alpine'
            args '-v /tmp/npm-cache:/root/.npm'
        }
    }
 
    stages {
        stage('Install & Test') {
            steps {
                sh 'node --version'
                sh 'npm ci'
                sh 'npm test'
            }
        }
    }
}

The node:20-alpine image provides a locked-down Node.js version 20 — every build runs with an identical toolchain, regardless of what is installed on the host machine. The args argument passes extra options to docker run, here a volume for the npm cache so dependency installation is not downloaded from scratch every time.

A container does not have to cover the entire pipeline. A Docker agent can also be applied per stage, useful when different stages need different toolchains:

JenkinsPer-stage Docker agent
stage('Build Frontend') {
    agent {
        docker {
            image 'node:20-alpine'
        }
    }
    steps {
        sh 'npm run build'
    }
}
 
stage('Build Backend') {
    agent {
        docker {
            image 'maven:3.9-eclipse-temurin-17'
        }
    }
    steps {
        sh 'mvn -q package'
    }
}

Note

The Docker agent depends on the Docker plugin and a Docker daemon on the agent node. Distinguish two easily confused things: agent { docker } runs pipeline steps inside a container (the toolchain), whereas building an image requires a Docker CLI connected to a daemon — the next topic. The two are often used together.

Building a Docker Image from Jenkins

Once the application is ready, the end goal is a container image. Building an image means invoking docker build, and that requires the Docker CLI inside the container agent plus access to a Docker daemon. This is where an important architectural choice with major security consequences arises.

Docker-in-Docker (DinD). This approach runs a separate Docker daemon inside the container. The inner container is wrapped by the outer container. Fully isolated, but heavier, with a real overhead.

Host socket mount. This approach mounts the host daemon socket into the container agent with the volume /var/run/docker.sock. The container agent can then command the host daemon directly — fast, no extra daemon, and the most common way in CI pipelines. The consequence: the container agent gets full control over the host daemon, which is equivalent to root access on that host.

JenkinsContainer agent with the host socket
pipeline {
    agent {
        docker {
            image 'docker:24'
            args '-v /var/run/docker.sock:/var/run/docker.sock'
        }
    }
 
    stages {
        stage('Build Image') {
            steps {
                sh 'docker build -t company/api-service .'
            }
        }
    }
}

Warning

Mounting /var/run/docker.sock grants root-equivalent access to the host. If the container agent can be reached by untrusted parties — for example pipelines from forks or external PRs — it becomes a direct RCE path to the server. The Jenkins security rule from episode 3 still applies: never execute builds from untrusted code on an agent that has access to the host daemon. In highly restricted environments, preference is given to DinD or a separate, specially scoped Docker daemon.

Docker Registry Integration

Built images must reach a registry so they can be deployed. For secure authentication, Jenkins provides the docker.withRegistry helper, which combines the registry URL with credentials from the credentials store — without ever writing a password in the Jenkinsfile:

JenkinsBuild and push an image with authentication
stage('Build & Push Image') {
    steps {
        script {
            docker.withRegistry('https://index.docker.io/v1/', 'docker-hub-credentials') {
                def image = docker.build("company/api-service:${env.GIT_COMMIT}")
                image.push()
                image.push('latest')
            }
        }
    }
}

How to read it: docker.withRegistry opens an authenticated session to the registry, then the block inside can build and push images. The docker-hub-credentials credentials are taken from the credentials store — just fill them in once in Manage Jenkins. The image is tagged uniquely by commit, then pushed with two tags: one immutable tag per commit (for precise rollbacks) and one latest tag for deployment convenience.

Multi-Stage Dockerfile Example

A good production image separates the build phase from the runtime phase so the final image is as small as possible. The following multi-stage Dockerfile builds the frontend application in the builder stage, then only copies the final output into a slim nginx image:

Multi-stage Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM nginx:1.27-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80

The first stage downloads and locks the dependencies, then builds the application. The second stage does not carry Node.js, source code, or node_modules — only the dist output is copied into the nginx image. The result is an image of tens of MB instead of hundreds, with a much smaller attack surface.

Complete Pipeline Example

Combining everything: the build runs in a sterile container agent, the multi-stage image is built, and it is pushed to the registry with securely stored authentication:

JenkinsEnd-to-end Docker pipeline
pipeline {
    agent {
        docker {
            image 'docker:24'
            args '-v /var/run/docker.sock:/var/run/docker.sock'
        }
    }
 
    stages {
        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
 
        stage('Build & Push Image') {
            steps {
                script {
                    docker.withRegistry('https://index.docker.io/v1/', 'docker-hub-credentials') {
                        def image = docker.build("company/api-service:${env.GIT_COMMIT}")
                        image.push()
                        image.push('latest')
                    }
                }
            }
        }
    }
 
    post {
        cleanup {
            cleanWs()
        }
    }
}

Tip

Note that this complete pipeline uses the host socket, which is fast and simple. For more serious environments, consider setting up a Docker agent that already carries a private daemon, or wait for episode 13 on Jenkins Configuration as Code to manage this agent configuration in a repeatable, auditable way.

Conclusion

In episode 8 we dissected Docker's role in a Jenkins pipeline from three directions: running steps inside a container agent with agent { docker { image ... } } for a sterile, reproducible toolchain; building images from within the pipeline using the Docker CLI with two daemon architectures — the isolated Docker-in-Docker or the fast-but-risky host socket mount — and pushing images to a registry securely with docker.withRegistry and the credentials store. We also looked at a multi-stage Dockerfile that separates builder from runtime for a slim image.

The key takeaways to carry with you:

  • agent { docker { image 'node:20-alpine' } } runs the entire pipeline in a locked, uniform environment.
  • Per-stage Docker agents let one pipeline use a different toolchain at each phase.
  • Mounting /var/run/docker.sock is equivalent to granting root access to the host; avoid it on builds that accept untrusted code.
  • docker.withRegistry(url, credentialsId) keeps image push authentication securely stored without a password in the Jenkinsfile.
  • A multi-stage Dockerfile cuts image size and shrinks the attack surface.

Your pipeline now produces deploy-ready container images. In episode 9 we face a different scale problem: reusability — when the same Jenkinsfile must be used by dozens of repositories, copy-paste becomes a nightmare. We will cover Jenkins Shared Libraries to centralize pipeline logic in a single team-managed repo. See you in episode 9!

Learn Jenkins - Docker Integration Inside a Jenkins Pipeline | Learn Jenkins