In a declarative Jenkinsfile, what does `agent { docker { image 'maven:3.9-eclipse-temurin-17' } }` actually do, and why do steps inside it often fail writing to the home directory?
answer
- a wrapper, not a separate node
- workspace is bind-mounted from the host
- runs as the agent's uid on purpose
- no passwd entry, no writable HOME
- reuseNode keeps the outer workspace
basics
~20 sJenkins picks a node with a working Docker daemon, starts that image as a long-running container with the workspace bind-mounted, and runs each step inside it as the agent's own user id. That uid usually has no passwd entry in the image, so its home directory is not writable.
solid answer
~60 sThe Docker Pipeline plugin turns `agent { docker { … } }` into: choose a node that can run Docker (narrow it with the nested `label` option), pull the image, then `docker run` it detached with the workspace bind-mounted at the same path and the working directory set to it. Each `sh` step is then executed inside that container rather than on the host. When the stage or pipeline ends, the container is removed, and the workspace — which lives on the host — survives. The permission problem comes from how Jenkins invokes it: the container runs as the *agent's* uid and gid so files written into the mounted workspace stay owned by the Jenkins user. That uid normally has no entry in the image's `/etc/passwd`, so `HOME` resolves somewhere unwritable and tools that write dotfiles — Maven's `~/.m2`, npm, pip, git config — fail. The usual fixes are to point HOME at a writable path or mount a cache directory through the nested `args` option, or to bake a matching user into a custom image.
code
groovy · 16 linespipeline {
agent {
docker {
image 'maven:3.9-eclipse-temurin-17'
label 'docker'
args '-v /srv/cache/m2:/var/maven/.m2 -e MAVEN_CONFIG=/var/maven/.m2'
}
}
stages {
stage('Build') {
steps {
sh 'mvn -Duser.home=/var/maven -B verify'
}
}
}
}go deeper
Know that this runs your build steps inside the named container image on an agent that has Docker, and that the workspace is shared with the container so build output ends up on the agent as usual.
Explain the expansion: node selection, pull, a detached docker run with the workspace bind-mounted and -u set to the agent's uid, steps via the container, removal at the end — and why that uid explains the home-directory failures.
Show the operational fixes: label so only Docker-capable nodes are picked, args for cache mounts and HOME, reuseNode for stage-level agents, and the trust consequence of mounting the host socket or requiring privileged mode.
Decide when container agents are enough and when the fleet should move to pod-per-build. Weigh toolchain-as-code via dockerfile true against build time, image-cache warmth, and the fact that a build with the host socket is effectively root on that agent.
## What the directive expands to `agent { docker { … } }` comes from the Docker Pipeline plugin, and it is best understood as a wrapper, not a new kind of agent. Jenkins still allocates an ordinary agent — a real node with an executor and a workspace. What changes is where the *steps* run. Roughly, the sequence is: 1. Allocate a node. By default any node; with the nested `label` option, only nodes carrying that label. That node must have a usable Docker daemon, because the plugin shells out to the `docker` CLI there. 2. `docker pull` the image (unless `alwaysPull false` and it is already present). 3. `docker run -t -d` the image with the workspace bind-mounted into the container at the same absolute path, the working directory set to it, and `-u <uid>:<gid>` matching the user the Jenkins agent process runs as. 4. Execute each `sh` step inside that running container. 5. Stop and remove the container when the block ends. Because the workspace is a bind mount from the host, everything the build writes persists on the agent after the container is gone. That is the whole point: a disposable toolchain over a persistent workspace. ## Why the home directory breaks Step 3 is where the classic failure comes from. Jenkins deliberately runs as the host's Jenkins uid so that files created in the workspace are owned by the Jenkins user rather than root — otherwise the next build could not clean up after this one. But a numeric uid like 1000 or 995 usually has no entry in the image's `/etc/passwd`. The consequences cascade: - `whoami` prints an error like `cannot find name for user ID 1000`. - `HOME` may be unset or point at `/`, which is not writable by a non-root user. - Maven tries to create `~/.m2/repository` and fails. npm tries `~/.npm`. pip tries `~/.cache/pip`. `git config --global` tries `~/.gitconfig`. All fail with permission-denied errors on paths that have nothing to do with your build. The standard remedies, in order of preference: ```groovy // Point HOME at a writable path and cache dependencies deliberately agent { docker { image 'maven:3.9-eclipse-temurin-17' label 'docker' args '-v $HOME/.m2:/var/maven/.m2 -e MAVEN_CONFIG=/var/maven/.m2' } } ``` plus passing `-Duser.home=/var/maven` to Maven, or more simply setting the tool's own cache location through an environment variable. Building your own image with a user whose uid matches the agent's is the most robust option for a fleet you control. Running with `-u root` makes the error go away and leaves root-owned files in the workspace that later builds cannot delete — it trades a loud failure for a quiet one. ## The options that matter - `label` — restrict to nodes that actually have Docker. Without it, a node with no daemon will be selected and the stage fails at `docker run`. - `args` — extra arguments appended to `docker run`: volume mounts, environment variables, network settings. - `alwaysPull` — force a pull each build so a moving tag does not go stale on one agent while it is current on another. - `registryUrl` / `registryCredentialsId` — pull from a private registry. - `reuseNode` — for a *stage-level* docker agent inside a pipeline that already has a top-level agent, run the container on that same node and reuse the same workspace, instead of allocating a second node. Without it, the stage gets a different node and a fresh workspace, and you are back to the missing-file problem. - `agent { dockerfile true }` — build the image from a `Dockerfile` in the repository (with `filename`, `dir` and `additionalBuildArgs` options) instead of pulling a published one. Useful when the toolchain is versioned with the code; it costs a build per pipeline unless the layer cache on that node is warm. ## Where this sits against the alternatives A Docker agent gives per-build toolchain isolation while still depending on a persistent host: the daemon, its image cache and the workspace all belong to a machine you maintain. It removes "works on agent 3, fails on agent 7" caused by different JDKs, but it does not give you a clean machine — the workspace is still reused, and anything written outside the mounted workspace inside the container is gone. If you want a genuinely fresh environment per build, the Kubernetes plugin's pod-per-build model is the step beyond this. One more caution: builds that themselves run Docker commands inside a Docker agent need access to a daemon. Mounting the host's socket gives the build effective root on the agent, and Docker-in-Docker needs a privileged container. Both are real choices with real trust consequences — a build container with the host socket mounted is not isolated from the host in any meaningful sense.
- What does reuseNode true change for a stage-level docker agent?Without it, a stage-level `docker` agent allocates a *different* node and a fresh workspace, so build output from earlier stages is missing. With `reuseNode true` the container is started on the node the pipeline already holds and the same workspace is mounted, so the stage sees the existing files and no second executor is consumed.
- Why is running the container as root a poor fix for the home-directory errors?It works, but every file the build writes into the bind-mounted workspace is then owned by root on the host. Later builds running as the Jenkins user cannot modify or delete them, so workspace cleanup fails and the next build breaks in a way that looks unrelated. Point HOME at a writable path or bake a matching uid into the image instead.
- What changes when you use agent { dockerfile true } instead of a published image?Jenkins builds the image from a `Dockerfile` in the checked-out repository on the selected node, then runs the stage inside it. The toolchain is versioned with the code and reviewed like code, at the cost of an image build per pipeline on any node whose layer cache is cold. Options such as `filename`, `dir` and `additionalBuildArgs` control what gets built.
saying these in an interview costs you the question
- Thinking the docker agent replaces the need for a real node
- Expecting files written outside the workspace to survive
- Fixing permission errors by always running as root
- Assuming the image ships a user matching the agent's uid
- Mounting the host Docker socket without noting it grants root