Before I containerized Jenkins, upgrading it meant a nervous SSH session into a VM, crossing my fingers that the apt upgrade wouldn’t break some plugin dependency. Now Jenkins upgrades are a one-line docker pull and a container restart, with rollback just as easy. Running Jenkins in Docker isn’t just a convenience thing – it fundamentally changes how safely you can experiment, upgrade, and reproduce your CI environment. Here’s the complete setup.
Why Run Jenkins in Docker
- Reproducibility – your Jenkins environment is defined in a Dockerfile, not built up manually over years through the UI.
- Easy upgrades and rollbacks – swap the image tag, and if something breaks, roll back to the previous tag instantly.
- Isolation – Jenkins and its dependencies don’t pollute the host OS, and you can run multiple Jenkins instances side by side for testing.
- Portability – the exact same image runs identically on your laptop, a VM, or inside Kubernetes.
Jenkins-in-Docker Architecture
There are two distinct things to containerize, and it’s important not to conflate them:
- The Jenkins controller – the web UI, job configs, and orchestration logic, running as a long-lived container with persistent storage.
- Build agents – ephemeral containers spun up to actually execute your builds, which can be plain Docker containers, Docker-in-Docker, or (as covered in the Kubernetes article) dynamic pods.
Step 1: Run the Jenkins Controller with Docker
The simplest starting point:
docker volume create jenkins_home
docker run -d \
--name jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
jenkins/jenkins:lts-jdk17
- Port
8080serves the web UI. - Port
50000is used for JNLP agent connections. - The named volume
jenkins_homepersists all Jenkins state (jobs, plugins, config) across container restarts and image upgrades.
Grab the initial admin password:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
Step 2: Build a Custom Jenkins Image
Rather than installing plugins by hand every time, bake them into a custom image so setup is fully reproducible:
FROM jenkins/jenkins:lts-jdk17
USER root
# Install Docker CLI so Jenkins can talk to the host Docker daemon
RUN apt-get update && apt-get install -y \
apt-transport-https ca-certificates curl gnupg lsb-release \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list \
&& apt-get update && apt-get install -y docker-ce-cli \
&& rm -rf /var/lib/apt/lists/*
USER jenkins
COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt
COPY jenkins.yaml /var/jenkins_home/casc_configs/jenkins.yaml
ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc_configs/jenkins.yaml
Build and run it:
docker build -t mycompany/jenkins:1.0 .
docker run -d --name jenkins -p 8080:8080 -p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
mycompany/jenkins:1.0
Step 3: Let Jenkins Build and Run Docker Images (Docker-in-Docker vs Socket Mounting)
There are two common approaches for letting pipelines run docker build:
Option A: Mount the host Docker socket (simplest, but gives the container root-equivalent access to the host):
-v /var/run/docker.sock:/var/run/docker.sock
Option B: Docker-in-Docker (dind) – run an isolated Docker daemon inside its own container, which is safer for multi-tenant setups but has more overhead:
# docker-compose.yml
services:
jenkins:
image: mycompany/jenkins:1.0
ports:
- "8080:8080"
- "50000:50000"
volumes:
- jenkins_home:/var/jenkins_home
environment:
- DOCKER_HOST=tcp://docker:2376
- DOCKER_CERT_PATH=/certs/client
- DOCKER_TLS_VERIFY=1
volumes:
- jenkins-docker-certs:/certs/client
docker:
image: docker:24-dind
privileged: true
environment:
- DOCKER_TLS_CERTDIR=/certs
volumes:
- jenkins-docker-certs:/certs/client
- jenkins-data:/var/jenkins_home
volumes:
jenkins_home:
jenkins-docker-certs:
jenkins-data:
For most single-tenant internal CI setups, socket mounting is simpler and perfectly adequate; reserve dind for cases where build isolation is a hard requirement.
Step 4: Configure Jenkins as Code
Instead of clicking through setup screens, define your controller config declaratively:
# jenkins.yaml
jenkins:
systemMessage: "Jenkins configured via JCasC"
numExecutors: 2
securityRealm:
local:
allowsSignup: false
users:
- id: admin
password: ${JENKINS_ADMIN_PASSWORD}
authorizationStrategy:
loggedInUsersCanDoAnything:
allowAnonymousRead: false
unclassified:
location:
url: http://jenkins.mycompany.com/
Pass JENKINS_ADMIN_PASSWORD as an environment variable or Docker secret at container start rather than hard-coding it.
Step 5: Use Docker Agents Directly in a Jenkinsfile
You don’t need a full Kubernetes cluster to get ephemeral, containerized build environments – Jenkins can spin up a Docker container per stage using the docker agent directive, provided the controller can reach the Docker daemon:
pipeline {
agent none
stages {
stage('Build') {
agent {
docker {
image 'maven:3.9-eclipse-temurin-17'
args '-v $HOME/.m2:/root/.m2'
}
}
steps {
sh 'mvn -B clean package'
}
}
stage('Build Image') {
agent any
steps {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
sh 'docker push myregistry.com/myapp:${BUILD_NUMBER}'
}
}
}
}
Each stage gets a clean, disposable container, keeping your build environment consistent no matter which Jenkins agent happens to pick it up.
Backing Up and Restoring Jenkins Data
Because everything lives in the jenkins_home volume, backups are straightforward:
docker run --rm -v jenkins_home:/var/jenkins_home -v $(pwd):/backup \
alpine tar czf /backup/jenkins_backup_$(date +%F).tar.gz -C /var/jenkins_home .
Restore by extracting that archive back into a fresh volume before starting the container.
Integrating with the Wider Toolchain
- Git/GitHub – webhook-triggered builds work identically whether Jenkins is containerized or not; just ensure the container’s exposed port is reachable from GitHub.
- Kubernetes – the same image you build here is exactly what you’d deploy to a cluster (see the companion article on running Jenkins on Kubernetes) – Docker Compose is a great local/staging step before that jump.
- Terraform/Ansible – provision the host VM and Docker installation itself via Terraform, then use Ansible or a simple
docker runscript for the Jenkins container lifecycle. - Monitoring tools – expose the Prometheus plugin’s
/prometheusmetrics endpoint and scrape it from a containerized Prometheus/Grafana stack running alongside Jenkins.
Monitoring and Troubleshooting
- Permission denied on docker.sock – the
jenkinsuser inside the container needs to be in a group matching the host’s Docker group GID; addusermod -aG docker jenkinsin your Dockerfile or match GIDs explicitly. - Jenkins losing data after container recreation – almost always means the volume wasn’t mounted correctly; double-check
-v jenkins_home:/var/jenkins_homeis present every time you recreate the container. - Out of disk space – Jenkins workspaces and Docker images both accumulate quickly; schedule
docker system pruneand Jenkins workspace cleanup (via the Workspace Cleanup plugin) regularly. - Container restarts losing plugin installs done via UI – if you’re baking plugins into the image, UI-installed plugins not in
plugins.txtwill vanish on the next rebuild; that’s expected behavior, add them toplugins.txtinstead.
Security Best Practices
- Avoid mounting the Docker socket into the Jenkins container in genuinely multi-tenant environments – it effectively grants root on the host to anyone with build-script access.
- Run the container as a non-root user where possible – the official
jenkins/jenkinsimage already does this by default. - Keep the base image patched; rebuild and redeploy on a schedule rather than running the same image for a year.
- Store secrets via Docker secrets or Jenkins Credentials, never as plain environment variables baked into the image.
Best Practices
- Treat the Jenkins container as disposable and the volume as the source of truth – never store anything important only inside the container’s writable layer.
- Version-control your Dockerfile,
plugins.txt, and JCasC YAML together so the entire controller setup can be rebuilt from Git. - Use Docker Compose (or a small set of
docker runscripts) rather than manual ad hoc commands, so the setup is repeatable across environments. - Tag your custom Jenkins images with meaningful versions, not just
latest, so rollbacks are unambiguous.
FAQs
Should the Jenkins controller itself run builds, or only agents? Only agents, ideally – keep the controller container lightweight and dedicate all execution to ephemeral agent containers, whether Docker or Kubernetes-based.
Can I run Jenkins in Docker on Windows? Yes, using Windows containers or WSL2-backed Docker Desktop, though most production Jenkins-in-Docker setups target Linux containers for simplicity.
Is Docker Compose enough for production, or do I need Kubernetes? Docker Compose is fine for small teams or single-host setups; once you need multi-node scaling, self-healing, or elastic agents, Kubernetes becomes the better fit.
How do I move from a bare-metal Jenkins install to Docker? Copy your existing $JENKINS_HOME directory into the new named volume before starting the containerized version – Jenkins will pick up all existing jobs, credentials, and config.
Does containerizing Jenkins slow builds down? Not meaningfully – the overhead is in container startup for ephemeral agents (usually a few seconds), which is a fair trade for consistency and isolation.
Summary
Running Jenkins in Docker gives you a controller that’s easy to back up, upgrade, and reproduce, plus the option to run every build in a clean, disposable container. Start with the official jenkins/jenkins image and a persistent volume, layer in a custom Dockerfile with your plugins baked in via plugins.txt, configure the controller declaratively with JCasC, and use the docker agent directive in your Jenkinsfiles for consistent, isolated build environments. It’s a natural stepping stone toward Kubernetes if your scaling needs grow, but stands entirely on its own for small-to-mid-sized teams.
References
- Official Jenkins Docker image documentation: https://github.com/jenkinsci/docker
- Jenkins Configuration as Code plugin: https://plugins.jenkins.io/configuration-as-code/
- Docker Pipeline Plugin: https://plugins.jenkins.io/docker-workflow/
- Docker-in-Docker official image: https://hub.docker.com/_/docker
- Jenkins Pipeline Syntax reference: https://www.jenkins.io/doc/book/pipeline/syntax/
